@panphora/clayjs 1.16.0 → 1.18.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 (511) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +72519 -59839
  11. package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
  12. package/dist/m/attrs/save-freeze-LWGF6XUN.js +1 -0
  13. package/dist/m/c-2GRUM77Q.js +30 -0
  14. package/dist/m/c-2KIEGNC4.js +1 -0
  15. package/dist/m/{c-5R4PTRG2.js → c-2KL3BD52.js} +1 -1
  16. package/dist/m/c-2YYDMLNZ.js +1 -0
  17. package/dist/m/c-2ZZBC45O.js +1 -0
  18. package/dist/m/c-372N6H3B.js +1 -0
  19. package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
  20. package/dist/m/c-3LDGIBMN.js +1 -0
  21. package/dist/m/c-3O7OBDCM.js +1 -0
  22. package/dist/m/c-4ASQPRQL.js +1 -0
  23. package/dist/m/c-4TXNLX4K.js +6 -0
  24. package/dist/m/c-4YU3BR4D.js +6 -0
  25. package/dist/m/c-5734MF2P.js +1 -0
  26. package/dist/m/c-57XDY6HC.js +1 -0
  27. package/dist/m/{c-VFX45IZQ.js → c-5B4YBZXC.js} +1 -1
  28. package/dist/m/c-5ONTYIUI.js +1 -0
  29. package/dist/m/c-5OOVFCJM.js +1 -0
  30. package/dist/m/c-5T6NZHNM.js +1 -0
  31. package/dist/m/{c-YDH7Q3NY.js → c-5ZQV7KC4.js} +1 -1
  32. package/dist/m/{c-Z36J63PP.js → c-6DB2OLRJ.js} +1 -1
  33. package/dist/m/c-6DNRCPNM.js +1 -0
  34. package/dist/m/c-6UGWFMQC.js +2 -0
  35. package/dist/m/c-7K4SCQB7.js +1 -0
  36. package/dist/m/c-7MG5RU63.js +1 -0
  37. package/dist/m/c-7PRJCA3E.js +1 -0
  38. package/dist/m/c-A6L3FMUS.js +1 -0
  39. package/dist/m/c-AMU3BPPR.js +4 -0
  40. package/dist/m/c-AMYEFA2E.js +1 -0
  41. package/dist/m/c-AQYCG7QW.js +1 -0
  42. package/dist/m/c-ATKAOH23.js +1 -0
  43. package/dist/m/c-B3JM5ZXC.js +1 -0
  44. package/dist/m/c-B5CS55WR.js +1 -0
  45. package/dist/m/c-B5UJQT7W.js +1 -0
  46. package/dist/m/c-B7J4K2AE.js +3 -0
  47. package/dist/m/c-BHZYSHW4.js +1 -0
  48. package/dist/m/c-BIJALSRD.js +1 -0
  49. package/dist/m/c-BJ7YQ5QZ.js +1 -0
  50. package/dist/m/c-BPF2NSPV.js +1 -0
  51. package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
  52. package/dist/m/c-BUAYFEV5.js +1 -0
  53. package/dist/m/c-C6IPRG34.js +1 -0
  54. package/dist/m/{c-2JL6E75N.js → c-CAS3NND4.js} +1 -1
  55. package/dist/m/{c-4DRMS4QG.js → c-CCQ25GFY.js} +1 -1
  56. package/dist/m/c-CJFDLD43.js +1 -0
  57. package/dist/m/c-COPTL5WB.js +1 -0
  58. package/dist/m/c-D3E4WJ4M.js +166 -0
  59. package/dist/m/c-D5IKEKXW.js +7 -0
  60. package/dist/m/c-DHBRA6DL.js +1 -0
  61. package/dist/m/c-DKLS74XB.js +72 -0
  62. package/dist/m/c-DVYGFTSK.js +3 -0
  63. package/dist/m/c-E3ZXARWA.js +1 -0
  64. package/dist/m/c-EK63G3CG.js +1 -0
  65. package/dist/m/c-ERQSNR6K.js +13 -0
  66. package/dist/m/c-EVYAISP3.js +1 -0
  67. package/dist/m/c-EXKHPOVC.js +3 -0
  68. package/dist/m/{c-7VGZ7RU5.js → c-FWKHM4E7.js} +1 -1
  69. package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
  70. package/dist/m/c-GNDL3TKB.js +1 -0
  71. package/dist/m/c-GRWMD6O5.js +1 -0
  72. package/dist/m/c-GRXHHFPO.js +1 -0
  73. package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
  74. package/dist/m/c-HAWFNBYG.js +1 -0
  75. package/dist/m/c-HG2C77BM.js +4 -0
  76. package/dist/m/c-HHGJBU4D.js +1 -0
  77. package/dist/m/c-HMV6P4RX.js +1 -0
  78. package/dist/m/c-HMYWIHFG.js +1 -0
  79. package/dist/m/c-HT2DKBV2.js +1 -0
  80. package/dist/m/c-I2SCCUEW.js +1 -0
  81. package/dist/m/{c-HRDCD3WN.js → c-ICJYHABF.js} +2 -2
  82. package/dist/m/c-IMZCWYHR.js +7 -0
  83. package/dist/m/c-IVMD3FEM.js +1 -0
  84. package/dist/m/c-IVTRVENZ.js +1 -0
  85. package/dist/m/c-IW6GGOU2.js +1 -0
  86. package/dist/m/c-JKZJIUIP.js +1 -0
  87. package/dist/m/{c-NZMNXVF6.js → c-JNLX3RJB.js} +1 -1
  88. package/dist/m/c-JRKY44DK.js +1 -0
  89. package/dist/m/c-K3ADYNLU.js +1 -0
  90. package/dist/m/c-KBS3UV6B.js +1 -0
  91. package/dist/m/c-KGHVFRR3.js +1 -0
  92. package/dist/m/c-KJJPV77R.js +1 -0
  93. package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
  94. package/dist/m/{c-5Y6TNX5W.js → c-KP64LT2A.js} +2 -2
  95. package/dist/m/c-KQ7E3PYY.js +1 -0
  96. package/dist/m/c-L3F5ZLIV.js +1 -0
  97. package/dist/m/{c-LR4CGEPT.js → c-L42OLXWM.js} +3 -2
  98. package/dist/m/c-L5VFATT6.js +1 -0
  99. package/dist/m/c-L7JL5TL3.js +3 -0
  100. package/dist/m/c-LKSL4AUJ.js +1 -0
  101. package/dist/m/c-M6FNQCTQ.js +1 -0
  102. package/dist/m/c-MCOOHCSG.js +1 -0
  103. package/dist/m/c-MHI3GYKF.js +1 -0
  104. package/dist/m/c-MWXFLW4J.js +1 -0
  105. package/dist/m/c-NEL7HQW3.js +1 -0
  106. package/dist/m/c-NQJRVJ56.js +1 -0
  107. package/dist/m/c-NWX3MFT4.js +1 -0
  108. package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
  109. package/dist/m/c-OKXI52N7.js +1 -0
  110. package/dist/m/{c-MMUIUUZG.js → c-OPLUZR3A.js} +1 -1
  111. package/dist/m/{c-J7CA54IP.js → c-OTMKN2OO.js} +1 -1
  112. package/dist/m/{c-LTGMXXKO.js → c-OYRZ7XG2.js} +1 -1
  113. package/dist/m/c-P3OFYUMA.js +1 -0
  114. package/dist/m/c-PLN76GF3.js +6 -0
  115. package/dist/m/c-PLP4JKFC.js +1 -0
  116. package/dist/m/c-Q26MME6N.js +1 -0
  117. package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
  118. package/dist/m/{c-SWRJFCNN.js → c-QIKT6SKM.js} +1 -1
  119. package/dist/m/c-QIXT3Q7C.js +28 -0
  120. package/dist/m/c-QO4HNGCQ.js +1 -0
  121. package/dist/m/c-R27DWQKX.js +1 -0
  122. package/dist/m/{c-HVYXIS72.js → c-R2XUODVY.js} +1 -1
  123. package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
  124. package/dist/m/c-RND3G2S2.js +1 -0
  125. package/dist/m/c-S2U6NJU4.js +1 -0
  126. package/dist/m/c-SPZPGD3R.js +1 -0
  127. package/dist/m/c-SW4YEG2D.js +8 -0
  128. package/dist/m/c-SZGYW5HT.js +1 -0
  129. package/dist/m/c-T637T7SN.js +1 -0
  130. package/dist/m/c-TJAXGIRV.js +1 -0
  131. package/dist/m/{c-KKWLDOPM.js → c-TNEM3RNM.js} +1 -1
  132. package/dist/m/c-TTB5EK4S.js +32 -0
  133. package/dist/m/c-U7SHNWDH.js +1 -0
  134. package/dist/m/c-UDHIVLYH.js +20 -0
  135. package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
  136. package/dist/m/{c-TQZ2JYHD.js → c-UORXLSD3.js} +1 -1
  137. package/dist/m/c-UPD33ZPE.js +1 -0
  138. package/dist/m/c-URSMSFCB.js +1 -0
  139. package/dist/m/c-UY4QX42B.js +1 -0
  140. package/dist/m/c-V44KWI2L.js +4 -0
  141. package/dist/m/c-VG5XIN4T.js +1 -0
  142. package/dist/m/c-VILENZTD.js +1 -0
  143. package/dist/m/{c-33HUYEDI.js → c-VJYEVRYK.js} +1 -1
  144. package/dist/m/c-VR2BJ4AN.js +1 -0
  145. package/dist/m/c-W4JCVLAB.js +1 -0
  146. package/dist/m/c-W6DQSHXI.js +135 -0
  147. package/dist/m/c-WBGWGOM7.js +1 -0
  148. package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
  149. package/dist/m/c-WGW764WX.js +1 -0
  150. package/dist/m/c-WMRJKQ2V.js +5 -0
  151. package/dist/m/c-WNTYKCDG.js +1 -0
  152. package/dist/m/c-WQWIRWO3.js +72 -0
  153. package/dist/m/c-X5LNHET5.js +1 -0
  154. package/dist/m/c-X5TSXZYJ.js +2 -0
  155. package/dist/m/c-XGH4GS2T.js +1 -0
  156. package/dist/m/c-Y6F2FORL.js +1 -0
  157. package/dist/m/c-YBKHUAK7.js +10 -0
  158. package/dist/m/c-YBSS55KU.js +20 -0
  159. package/dist/m/c-YCLU4UNT.js +2 -0
  160. package/dist/m/c-YVZ2L7CE.js +1 -0
  161. package/dist/m/c-ZK4GIZH2.js +1 -0
  162. package/dist/m/c-ZRXJD4G6.js +1 -0
  163. package/dist/m/c-ZWBI4HYK.js +1 -0
  164. package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
  165. package/dist/m/core/admin-attrs-YY7T5FGU.js +1 -0
  166. package/dist/m/core/autosave-S6OMNDCK.js +1 -0
  167. package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
  168. package/dist/m/core/host-permission-6EVJGNPL.js +1 -0
  169. package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
  170. package/dist/m/core/{persist-HKHA66IY.js → persist-UKXV5KLA.js} +1 -1
  171. package/dist/m/core/recovery-DRFZRJED.js +1 -0
  172. package/dist/m/core/save-LYY7K6U7.js +1 -0
  173. package/dist/m/core/save-core-MKRPSEEO.js +1 -0
  174. package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
  175. package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
  176. package/dist/m/events/index-RN2JNGBV.js +1 -0
  177. package/dist/m/internals/index-TGLQINN3.js +1 -0
  178. package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
  179. package/dist/m/loader-3BBNZZGM.js +1 -0
  180. package/dist/m/manifest-F449FF98.js +1 -0
  181. package/dist/m/manifest-edit-4396A009.js +1 -0
  182. package/dist/m/manifest.json +2509 -1474
  183. package/dist/m/options/options-EJMS3K24.js +1 -0
  184. package/dist/m/page/editor-6LRB3GE4.js +304 -0
  185. package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
  186. package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
  187. package/dist/m/plugins/demo-OYO5GCAO.js +1 -0
  188. package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
  189. package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
  190. package/dist/m/plugins/live-AD4H6XSL.js +1 -0
  191. package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
  192. package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
  193. package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
  194. package/dist/m/plugins/people-JCHVPF7P.js +1 -0
  195. package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
  196. package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-KLGETK77.js} +2 -2
  197. package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
  198. package/dist/m/plugins/source-TJXOIOYY.js +1 -0
  199. package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
  200. package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
  201. package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
  202. package/dist/m/plugins/upload-NHGJXZK2.js +1 -0
  203. package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
  204. package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
  205. package/dist/m/plugins/wire-VBWXEXU2.js +1 -0
  206. package/dist/m/reader-core-4W2KYEWQ.js +77 -0
  207. package/dist/m/route/route-62CHXL5B.js +1 -0
  208. package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
  209. package/dist/m/ui/index-IVWN2QC4.js +1 -0
  210. package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
  211. package/dist/offline-manifest.json +1560 -889
  212. package/dist/page/themes/light-1.css +33 -8
  213. package/dist/server/core.cjs +1554 -311
  214. package/dist/server/offline-runtime.cjs +63 -63
  215. package/dist/server/offline-runtime.mjs +63 -63
  216. package/dist/server/page-compaction-worker.cjs +3710 -0
  217. package/dist/server/rooms.mjs +5 -0
  218. package/docs/guide.md +653 -101
  219. package/entries/sap.js +5 -5
  220. package/package.json +1 -1
  221. package/src/core/admin-contenteditable.js +5 -2
  222. package/src/core/admin-inputs.js +1 -0
  223. package/src/core/admin-onclick.js +2 -1
  224. package/src/core/boot-phase.js +2 -1
  225. package/src/core/conflict-download.js +3 -29
  226. package/src/core/conflict-notice-ui.js +4 -7
  227. package/src/core/download.js +32 -0
  228. package/src/core/host-meta.js +56 -0
  229. package/src/core/is-edit-mode.js +17 -1
  230. package/src/core/mode-key.js +125 -19
  231. package/src/core/mode-support.js +5 -0
  232. package/src/core/mode-transition.js +20 -6
  233. package/src/core/preserve-authored.js +4 -4
  234. package/src/core/recovery.js +121 -33
  235. package/src/core/save-accepted.js +33 -0
  236. package/src/core/save-core.js +111 -32
  237. package/src/core/save-job.js +108 -0
  238. package/src/core/save-prepare.js +53 -0
  239. package/src/core/save-size-notice.js +3 -1
  240. package/src/core/save-slices.js +15 -0
  241. package/src/core/save.js +557 -127
  242. package/src/core/serialize-steps.js +109 -0
  243. package/src/core/snapshot.js +209 -40
  244. package/src/core/source-map.js +83 -38
  245. package/src/core/stale-host-notice.js +1 -0
  246. package/src/core/upgrade-run.js +59 -8
  247. package/src/dom/active-element.js +5 -0
  248. package/src/lib/assets.js +1 -0
  249. package/src/lib/autosave-scheduler.js +3 -4
  250. package/src/lib/slice.js +65 -0
  251. package/src/lib/uimotion.js +774 -0
  252. package/src/lib/unsaved-state.js +2 -2
  253. package/src/live/fast-lane.js +10 -3
  254. package/src/live/local-lane.js +202 -11
  255. package/src/live/ops.js +16 -0
  256. package/src/live/page-compaction.js +162 -0
  257. package/src/live/server-lane.js +1 -0
  258. package/src/live/store.js +11 -3
  259. package/src/loader-logic.js +3 -1
  260. package/src/loader.js +14 -4
  261. package/src/page/adopt-boot.js +2 -1
  262. package/src/page/awareness.js +33 -2
  263. package/src/page/blocks.js +122 -27
  264. package/src/page/board-views.js +2 -16
  265. package/src/page/boot-pack.js +1 -1
  266. package/src/page/chart-tables.js +29 -0
  267. package/src/page/chart.js +411 -72
  268. package/src/page/checklist.js +10 -135
  269. package/src/page/checkpoint-proof.js +295 -0
  270. package/src/page/chrome.js +257 -40
  271. package/src/page/clipboard-shape.js +126 -0
  272. package/src/page/document-title-dom.js +12 -0
  273. package/src/page/document-title-runtime.js +15 -1
  274. package/src/page/document-title.js +4 -1
  275. package/src/page/drag.js +33 -3
  276. package/src/page/editor.js +207 -31
  277. package/src/page/emoji-data.js +162 -0
  278. package/src/page/export-assets.js +357 -0
  279. package/src/page/export-html.js +209 -0
  280. package/src/page/export-markdown.js +361 -0
  281. package/src/page/features/board.js +12 -3
  282. package/src/page/features/chart.js +11 -0
  283. package/src/page/features/checklist.js +428 -0
  284. package/src/page/features/dates.js +180 -0
  285. package/src/page/features/emoji.js +187 -0
  286. package/src/page/features/export-html.js +189 -0
  287. package/src/page/features/export-markdown.js +169 -0
  288. package/src/page/features/import-markdown.js +349 -0
  289. package/src/page/features/media.js +61 -5
  290. package/src/page/features/mentions.js +188 -0
  291. package/src/page/features/registry.js +24 -1
  292. package/src/page/features/table.js +375 -85
  293. package/src/page/features/title-cover.js +331 -0
  294. package/src/page/features/title-icon.js +213 -0
  295. package/src/page/features/toc.js +87 -0
  296. package/src/page/format-command.js +33 -0
  297. package/src/page/format-gate.js +68 -18
  298. package/src/page/format-host.js +71 -0
  299. package/src/page/format-native-commands.js +158 -0
  300. package/src/page/format-native.js +42 -0
  301. package/src/page/image-actions.js +64 -0
  302. package/src/page/import-markdown.js +262 -0
  303. package/src/page/inline.js +32 -8
  304. package/src/page/items.js +141 -11
  305. package/src/page/markdown.js +30 -2
  306. package/src/page/media.js +35 -119
  307. package/src/page/mention-picker.js +99 -0
  308. package/src/page/merge.js +1 -1
  309. package/src/page/placeholder.js +14 -1
  310. package/src/page/projection.js +156 -25
  311. package/src/page/recover.js +10 -12
  312. package/src/page/recovery-ui.js +464 -0
  313. package/src/page/rich/chart.js +63 -7
  314. package/src/page/save-projection.js +67 -0
  315. package/src/page/save-states.js +14 -4
  316. package/src/page/schema.js +52 -3
  317. package/src/page/table-paste.js +3 -1
  318. package/src/page/table-style.js +4 -4
  319. package/src/page/table.js +44 -2
  320. package/src/page/themes/content.css +25 -1
  321. package/src/page/themes/light-1.css +33 -8
  322. package/src/page/themes/light-1.source.css +8 -7
  323. package/src/page/toc-link.js +31 -0
  324. package/src/page/toc.js +5 -0
  325. package/src/page/view-hooks.js +58 -0
  326. package/src/page/view.js +13 -0
  327. package/src/page/ybind/binding.js +116 -6
  328. package/src/page/ybind/bridge.js +187 -20
  329. package/src/page/ybind/containment.js +162 -0
  330. package/src/page/ybind/doc.js +17 -9
  331. package/src/page/ybind/file-debt.js +63 -0
  332. package/src/page/ybind/layout.js +7 -2
  333. package/src/page/ybind/marks.js +2 -1
  334. package/src/page/ybind/moves.js +5 -4
  335. package/src/page/ybind/origins.js +106 -4
  336. package/src/page/ybind/parts.js +8 -1
  337. package/src/page/ybind/provider.js +125 -18
  338. package/src/page/ybind/recovery-proof.js +32 -9
  339. package/src/page/ybind/recovery-replay.js +905 -4
  340. package/src/page/ybind/recovery.js +174 -45
  341. package/src/page/ybind/table.js +67 -11
  342. package/src/page/ybind/walk-delta.js +258 -0
  343. package/src/page/ybind/walk.js +54 -29
  344. package/src/page/ybind/writer.js +1 -1
  345. package/src/plugins/ai-edit.js +13 -9
  346. package/src/plugins/indicator-chip.js +91 -0
  347. package/src/plugins/indicator.js +13 -80
  348. package/src/plugins/live.js +126 -15
  349. package/src/plugins/menu/actions.js +196 -0
  350. package/src/plugins/menu/people.js +279 -0
  351. package/src/plugins/menu/placement.js +121 -0
  352. package/src/plugins/menu/preferences.js +9 -0
  353. package/src/plugins/menu/seo.js +316 -397
  354. package/src/plugins/menu/style.js +171 -0
  355. package/src/plugins/menu/variants.js +296 -0
  356. package/src/plugins/menu.js +186 -183
  357. package/src/plugins/page-view.js +3 -2
  358. package/src/plugins/page.js +362 -43
  359. package/src/plugins/sortable.js +8 -1
  360. package/src/plugins/source.js +6 -0
  361. package/src/plugins/upgrade.js +51 -17
  362. package/src/plugins/upload-ui.js +12 -17
  363. package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
  364. package/src/plugins/visitor-drafts/intro-style.js +7 -2
  365. package/src/plugins/visitor-drafts/intro.js +7 -4
  366. package/src/plugins/visitor-drafts/owner-chip.js +8 -12
  367. package/src/plugins/visitor-drafts/prompt.js +11 -9
  368. package/src/plugins/visitor-drafts/source.js +4 -3
  369. package/src/plugins/visitor-drafts/store.js +138 -9
  370. package/src/plugins/visitor-drafts/ui-memory.js +2 -2
  371. package/src/plugins/visitor-drafts-owner.js +4 -6
  372. package/src/plugins/visitor-drafts.js +5 -3
  373. package/src/recovery/store.js +1 -1
  374. package/src/route/route.js +4 -0
  375. package/src/server/checkpoint/index.js +2 -2
  376. package/src/server/rooms/index.js +5 -1
  377. package/src/server/rooms/live-data-rooms.js +302 -50
  378. package/src/server/rooms/page-compaction-worker.js +12 -0
  379. package/src/server/rooms/page-compactor.js +92 -0
  380. package/src/server/rooms/page-room-conformance.js +461 -0
  381. package/src/source/parse-steps.js +94 -0
  382. package/src/source/session.js +233 -57
  383. package/src/sync/editing-sender.js +12 -0
  384. package/src/sync/live-sync-edit.js +22 -2
  385. package/src/sync/live-sync.js +150 -23
  386. package/src/sync/page-relay.js +52 -0
  387. package/src/sync/presence-store.js +44 -4
  388. package/src/sync/presence.js +6 -3
  389. package/src/sync/saved-stream.js +28 -14
  390. package/src/ui/bevel-controls.js +1 -0
  391. package/src/ui/bevel-dialog.js +5 -2
  392. package/src/ui/editor-ui.js +77 -0
  393. package/src/ui/page-colour.js +281 -0
  394. package/src/ui/toast.js +16 -7
  395. package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
  396. package/src/vendor/hyper-undo.vendor.js +1 -1
  397. package/src/vendor/hypercms.vendor.js +9 -9
  398. package/src/vendor/parse5.vendor.js +2 -2
  399. package/src/vendor/sap-core.vendor.js +5 -5
  400. package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
  401. package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
  402. package/dist/m/c-2M73252O.js +0 -1
  403. package/dist/m/c-2SPVQSHC.js +0 -1
  404. package/dist/m/c-35NBGMZB.js +0 -1
  405. package/dist/m/c-3IQRECYS.js +0 -1
  406. package/dist/m/c-4B6H4G5V.js +0 -1
  407. package/dist/m/c-4FFHZM32.js +0 -1
  408. package/dist/m/c-4YSCRICO.js +0 -1
  409. package/dist/m/c-54RCDCBU.js +0 -1
  410. package/dist/m/c-5M4YEKCX.js +0 -4
  411. package/dist/m/c-5V3MOVJT.js +0 -1
  412. package/dist/m/c-66O32MV3.js +0 -28
  413. package/dist/m/c-6HMB24EV.js +0 -1
  414. package/dist/m/c-7O6AFR65.js +0 -1
  415. package/dist/m/c-ANQTJ3B5.js +0 -2
  416. package/dist/m/c-AYVOREBE.js +0 -1
  417. package/dist/m/c-B2ZXPOS3.js +0 -3
  418. package/dist/m/c-BMIZKMPV.js +0 -1
  419. package/dist/m/c-BTDI4Y23.js +0 -1
  420. package/dist/m/c-CJZSLMVQ.js +0 -1
  421. package/dist/m/c-DBE6ORSI.js +0 -1
  422. package/dist/m/c-EDHUR4MX.js +0 -1
  423. package/dist/m/c-EY6UOM54.js +0 -1
  424. package/dist/m/c-F3CKJTJS.js +0 -1
  425. package/dist/m/c-F7Y6FLPY.js +0 -1
  426. package/dist/m/c-FA4DZVTC.js +0 -1
  427. package/dist/m/c-G2BUZITR.js +0 -1
  428. package/dist/m/c-GB3JTV6E.js +0 -1
  429. package/dist/m/c-GBGWXTX4.js +0 -1
  430. package/dist/m/c-GHBCCVG4.js +0 -1
  431. package/dist/m/c-GJAEGGXY.js +0 -1
  432. package/dist/m/c-GOWBDM6J.js +0 -1
  433. package/dist/m/c-IB52R237.js +0 -1
  434. package/dist/m/c-IKWNE4R4.js +0 -1
  435. package/dist/m/c-IU3IYEU6.js +0 -1
  436. package/dist/m/c-IZ2EBRAJ.js +0 -1
  437. package/dist/m/c-KR4UW4KW.js +0 -1
  438. package/dist/m/c-KY3VJGUA.js +0 -8
  439. package/dist/m/c-LFPHCXY3.js +0 -1
  440. package/dist/m/c-LRPUO2MX.js +0 -1
  441. package/dist/m/c-LXENEBV7.js +0 -1
  442. package/dist/m/c-NMJRXKPC.js +0 -8
  443. package/dist/m/c-OIBUTDXI.js +0 -1
  444. package/dist/m/c-PPOSCAQD.js +0 -1
  445. package/dist/m/c-QIX5BJYN.js +0 -1
  446. package/dist/m/c-QQHEX6HA.js +0 -1
  447. package/dist/m/c-R7RMANJI.js +0 -9
  448. package/dist/m/c-RI4NT75Y.js +0 -1
  449. package/dist/m/c-RM7J7BV2.js +0 -1
  450. package/dist/m/c-RUJKSUQW.js +0 -1
  451. package/dist/m/c-SGCCC4FW.js +0 -25
  452. package/dist/m/c-T4VO4OCB.js +0 -1
  453. package/dist/m/c-TQEMVJCS.js +0 -1
  454. package/dist/m/c-TR6RMDQ7.js +0 -3
  455. package/dist/m/c-UKHTLWUW.js +0 -1
  456. package/dist/m/c-UNN7LWCT.js +0 -1
  457. package/dist/m/c-UXBMRDZB.js +0 -235
  458. package/dist/m/c-V7PZZRG7.js +0 -1
  459. package/dist/m/c-VL52ADDR.js +0 -1
  460. package/dist/m/c-WNYBLIOZ.js +0 -1
  461. package/dist/m/c-WWDBYXLK.js +0 -1
  462. package/dist/m/c-XNNW2CWK.js +0 -1
  463. package/dist/m/c-Y6USGTS5.js +0 -5
  464. package/dist/m/c-YG2NJYS6.js +0 -135
  465. package/dist/m/c-Z4VRC7GU.js +0 -7
  466. package/dist/m/c-Z6K52OLP.js +0 -19
  467. package/dist/m/c-ZDY2MDAA.js +0 -21
  468. package/dist/m/c-ZUCOKOHT.js +0 -1
  469. package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
  470. package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
  471. package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
  472. package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
  473. package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
  474. package/dist/m/core/recovery-TBBSICE2.js +0 -1
  475. package/dist/m/core/save-WUCHTQBE.js +0 -1
  476. package/dist/m/core/save-core-FJVHT5M3.js +0 -1
  477. package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
  478. package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
  479. package/dist/m/events/index-23DPTUT4.js +0 -1
  480. package/dist/m/internals/index-WOVAP3RU.js +0 -1
  481. package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
  482. package/dist/m/loader-HXX6YNH5.js +0 -1
  483. package/dist/m/manifest-F22DEDAC.js +0 -1
  484. package/dist/m/manifest-edit-C3C7376B.js +0 -1
  485. package/dist/m/options/options-2LH76JQ3.js +0 -1
  486. package/dist/m/page/editor-XUTB2HB2.js +0 -189
  487. package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +0 -6
  488. package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
  489. package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
  490. package/dist/m/plugins/live-NUTTXSBS.js +0 -1
  491. package/dist/m/plugins/menu-HCKO3DSP.js +0 -138
  492. package/dist/m/plugins/page-KSUH7G6J.js +0 -2
  493. package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
  494. package/dist/m/plugins/people-AJJ32YCG.js +0 -1
  495. package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
  496. package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
  497. package/dist/m/plugins/source-KHK5UUSK.js +0 -1
  498. package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
  499. package/dist/m/plugins/upgrade-ZNYODP5Y.js +0 -1
  500. package/dist/m/plugins/upload-XSGNILUY.js +0 -1
  501. package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
  502. package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
  503. package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
  504. package/dist/m/reader-core-ILWLXVMP.js +0 -68
  505. package/dist/m/route/route-KOCZSEGN.js +0 -1
  506. package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
  507. package/dist/m/ui/index-EVHKMKBL.js +0 -1
  508. package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
  509. /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
  510. /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
  511. /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
@@ -8,16 +8,15 @@ import { schema } from "./schema.js";
8
8
  import { BlockSelection, siblingSpan, moveBlock, indentBlocks, outdentBlocks } from "./blocks.js";
9
9
  import { outlineOf } from "./outline-cache.js";
10
10
  import { currentSpan, selectBlocks, duplicateBlocks, deleteBlocks, asStep, typeIntoBlocks } from "./blocksel.js";
11
- import { openAltEditor, setImageAttrs } from "./media.js";
12
11
  import { BlockDrag } from "./drag.js";
13
12
  import { openLinkEditor } from "./link.js";
14
- import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnInto, insertAfter, rankItems, runItem, retype, canTurnInto, insertItemsFor } from "./items.js";
13
+ import { BLOCK_TYPES, MENU_ITEMS, blockTypeOf, turnBlocks, turnInto, insertAfter, rankItems, runItem, retype, canTurnInto, insertItemsFor, loadChartPart, runMenuCommand } from "./items.js";
15
14
  import toast from "../ui/toast-lazy.js";
16
- import { insertChartAfterTable } from "./chart.js";
17
15
  import { pickCsv } from "./csv-import.js";
18
16
  import { HISTORY_INTENT, typingIntent } from "./history-intent.js";
19
17
  import { composing } from "./markdown.js";
20
18
  import { announce } from "./announce.js";
19
+ import { REMOTE } from "./model.js";
21
20
 
22
21
  export { BLOCK_TYPES, blockTypeOf, turnInto, insertAfter } from "./items.js";
23
22
 
@@ -74,20 +73,24 @@ function copyBlockLink(id) {
74
73
  }
75
74
 
76
75
  export const ACTION_ITEMS = [
77
- { key: "up", label: "Move up", icon: "↑", hint: `${MOD}⇧↑`, run: (view) => moveBlock(-1)(view.state, view.dispatch) },
78
- { key: "down", label: "Move down", icon: "↓", hint: `${MOD}⇧↓`, run: (view) => moveBlock(1)(view.state, view.dispatch) },
79
- { key: "indent", label: "Indent", icon: "→", hint: "Tab", run: (view) => indentBlocks(view.state, view.dispatch) },
80
- { key: "outdent", label: "Outdent", icon: "←", hint: "⇧Tab", run: (view) => outdentBlocks(view.state, view.dispatch) },
81
- { key: "duplicate", label: "Duplicate", icon: "⧉", hint: `${MOD}D`, run: (view) => duplicateBlocks(view.state, view.dispatch) },
82
- { key: "delete", label: "Delete", icon: "×", hint: "Del", run: (view) => deleteBlocks(view.state, view.dispatch) },
83
- { key: "turn", label: "Turn into", icon: "↻", hint: "›", only: "text", run: (view, chrome) => chrome.openMenu("turn", chrome.plus) },
84
- { key: "link", label: "Copy link", icon: "🔗", run: (view, chrome, index) => copyBlockLink(view.state.doc.child(index).attrs.id) },
85
- { key: "alt", label: "Alt text…", icon: "Aa", only: "image", focus: false, run: (view, chrome, index) => openAltEditor(view, imageId(view, index)) },
86
- { key: "small", label: "Small", icon: "▫", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 240 }) },
87
- { key: "medium", label: "Medium", icon: "◻", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: 480 }) },
88
- { key: "full", label: "Full width", icon: "⬜", only: "image", run: (view, chrome, index) => setImageAttrs(view, imageId(view, index), { width: null }) },
89
- { key: "chart", label: "Chart this table", icon: "📊", only: "table", run: (view, chrome, index) => insertChartAfterTable(view, blockStart(view.state.doc, index)) },
90
- { key: "csv", label: "Import CSV…", icon: "⇧", only: "table", focus: false, run: (view, chrome, index) => pickCsv(view, null, view.state.doc.child(index).attrs.id) },
76
+ { key: "up", label: "Move up", icon: "up", hint: `${MOD}⇧↑`, run: (view) => moveBlock(-1)(view.state, view.dispatch) },
77
+ { key: "down", label: "Move down", icon: "down", hint: `${MOD}⇧↓`, run: (view) => moveBlock(1)(view.state, view.dispatch) },
78
+ { key: "indent", label: "Indent", icon: "indent", hint: "Tab", run: (view) => {
79
+ const before = view.state.doc;
80
+ indentBlocks(view.state, view.dispatch);
81
+ if (view.state.doc === before) { announce(view, "Indent works on list items and inside toggles."); toast("Indent works on list items and inside toggles."); }
82
+ } },
83
+ { key: "outdent", label: "Outdent", icon: "outdent", hint: "⇧Tab", run: (view) => outdentBlocks(view.state, view.dispatch) },
84
+ { key: "duplicate", label: "Duplicate", icon: "duplicate", hint: `${MOD}D`, run: (view) => duplicateBlocks(view.state, view.dispatch) },
85
+ { key: "delete", label: "Delete", icon: "trash", hint: "Del", run: (view) => deleteBlocks(view.state, view.dispatch) },
86
+ { key: "turn", label: "Turn into", icon: "turn", hint: "›", only: "text", run: (view, chrome) => chrome.openMenu("turn", chrome.plus) },
87
+ { key: "link", label: "Copy link", icon: "link", run: (view, chrome, index) => copyBlockLink(view.state.doc.child(index).attrs.id) },
88
+ { key: "alt", label: "Alt text…", icon: "alt", only: "image", focus: false, run: (view, chrome, index) => { const id = imageId(view, index); return import("./image-actions.js").then((part) => part.openAltEditor(view, id)); } },
89
+ { key: "small", label: "Small", icon: "small", only: "image", run: (view, chrome, index) => { const id = imageId(view, index); return import("./image-actions.js").then((part) => part.setImageAttrs(view, id, { width: 240 })); } },
90
+ { key: "medium", label: "Medium", icon: "medium", only: "image", run: (view, chrome, index) => { const id = imageId(view, index); return import("./image-actions.js").then((part) => part.setImageAttrs(view, id, { width: 480 })); } },
91
+ { key: "full", label: "Full width", icon: "full", only: "image", run: (view, chrome, index) => { const id = imageId(view, index); return import("./image-actions.js").then((part) => part.setImageAttrs(view, id, { width: null })); } },
92
+ { key: "chart", label: "Chart this table", icon: "chart", only: "table", run: (view, chrome, index) => { const at = blockStart(view.state.doc, index); return loadChartPart(view).then(({ insertChartAfterTable }) => insertChartAfterTable(view, at)); } },
93
+ { key: "csv", label: "Import CSV…", icon: "csv", only: "table", focus: false, run: (view, chrome, index) => pickCsv(view, null, view.state.doc.child(index).attrs.id) },
91
94
  ];
92
95
 
93
96
  const actionsFor = (st) => {
@@ -112,6 +115,21 @@ const ICONS = {
112
115
  ordered: svg('<path d="M7 4.5h6.5M7 8h6.5M7 11.5h6.5M2.5 3.5l1-.5v3M2.5 9.3c.2-.5.6-.8 1.1-.8.6 0 1 .4 1 .9 0 .9-2.1 1.4-2.1 2.6h2.1"/>'),
113
116
  check: svg('<rect x="2.5" y="2.5" width="11" height="11" rx="2.5"/><path d="M5.5 8.2l1.8 1.8 3.4-4"/>'),
114
117
  more: svg('<path stroke-width="2.2" d="M3 8h.01M8 8h.01M13 8h.01"/>'),
118
+ gear: svg('<g transform="scale(0.6667)" stroke-width="2.25"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></g>'),
119
+ close: svg('<path d="M4 4l8 8M12 4l-8 8"/>'),
120
+ up: svg('<path d="M8 13V3.5M4 7.5 8 3.5l4 4"/>'),
121
+ down: svg('<path d="M8 3v9.5M4 8.5l4 4 4-4"/>'),
122
+ indent: svg('<path d="M2.5 3.5h11M7 8h6.5M2.5 12.5h11M2.5 6l2.5 2-2.5 2"/>'),
123
+ outdent: svg('<path d="M2.5 3.5h11M7 8h6.5M2.5 12.5h11M5 6 2.5 8 5 10"/>'),
124
+ duplicate: svg('<rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 3.5v-.5A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v5A1.5 1.5 0 0 0 4 9.5h.5"/>'),
125
+ trash: svg('<path d="M3 4.5h10M6.5 4.5V3h3v1.5M4.5 4.5l.6 8.5h5.8l.6-8.5"/>'),
126
+ turn: svg('<path d="M3 5.5h9.5M10 3l2.5 2.5L10 8M13 10.5H3.5M6 8l-2.5 2.5L6 13"/>'),
127
+ chart: svg('<path d="M3 13V3M3 13h10.5M6 10.5V8M9 10.5V5.5M12 10.5V7"/>'),
128
+ csv: svg('<path d="M4 1.5h5L12.5 5v9.5h-8.5zM9 1.5V5h3.5M8 7.5v5M6 10.5l2 2 2-2"/>'),
129
+ alt: svg('<path d="M2.5 12.5 5.5 4l3 8.5M3.6 9.5h3.8M10 12.5V7.5M10 8.5c.4-.7 1.1-1 2-1h.5"/>'),
130
+ small: svg('<rect x="5" y="5" width="6" height="6" rx="1"/>'),
131
+ medium: svg('<rect x="3.5" y="3.5" width="9" height="9" rx="1.2"/>'),
132
+ full: svg('<rect x="1.5" y="3.5" width="13" height="9" rx="1.2"/>'),
115
133
  };
116
134
  const TOOLS = [
117
135
  ["strong", "Bold"],
@@ -129,6 +147,7 @@ const PHONE_TOOLS = new Set(["strong", "em", "link"]);
129
147
  const MORE_ACTIONS = new Set(["up", "down", "indent", "outdent"]);
130
148
  const PHONE_BAR_WIDTH = 204, PHONE_BAR_HEIGHT = 52, PHONE_INSET = 8;
131
149
  const LIST_KINDS = new Set(["bullet", "ordered", "check"]);
150
+ const SETTING_ROWS = [["assignees", "Checklist assignees"], ["dates", "Checklist due dates"]];
132
151
  const LINE_TYPES = new Set(["paragraph", "heading", "list_item"]);
133
152
 
134
153
  // The top-level blocks a toolbar command acts on. A nonempty selection's end is
@@ -217,6 +236,11 @@ function runList(view, kind) {
217
236
  if (tr.docChanged) view.dispatch(asStep(tr));
218
237
  }
219
238
 
239
+ // The four formats this release adds are commands the format gate may refuse, and
240
+ // a refusal is a notice rather than a change. The menu that offers them, and the
241
+ // commands behind it, load the first time one is chosen (format-native-commands.js).
242
+ const nativeMenu = (view, chrome, name) => import("./format-native-commands.js").then((menu) => menu.nativeMenu(view, name, chrome.pickedFrom ?? chrome.more));
243
+
220
244
  function runMark(view, name) {
221
245
  if (name === "link") return openLinkEditor(view);
222
246
  const st = view.state;
@@ -277,15 +301,28 @@ export const heldSlashRange = (st) => {
277
301
  return held ? { from: held.from, to: held.to } : null;
278
302
  };
279
303
 
280
- export function openPageMenu(view, anchor, items) {
304
+ export function openPageMenu(view, anchor, items, { persist = false } = {}) {
281
305
  const chrome = chromeFor.get(view);
282
306
  if (!chrome) return false;
283
307
  chrome.openMenu("external", anchor);
284
308
  chrome.menu.externalItems = items;
309
+ chrome.menu.persist = persist;
285
310
  chrome.renderMenu();
286
311
  return true;
287
312
  }
288
313
 
314
+ // A menu whose rows name their targets by id outlives a render of the control that
315
+ // opened it: the control that takes the old one's place keeps the popup open.
316
+ export function reanchorPageMenu(view, from, to) {
317
+ const chrome = chromeFor.get(view);
318
+ const menu = chrome?.menu;
319
+ if (!menu || menu.anchor !== from || !to || to === from) return false;
320
+ menu.anchor = to;
321
+ to.setAttribute("aria-expanded", "true");
322
+ view.dom.setAttribute("aria-controls", chrome.menuId);
323
+ return true;
324
+ }
325
+
289
326
  class Chrome {
290
327
  constructor(view, onAiTool) {
291
328
  this.view = view;
@@ -301,18 +338,32 @@ class Chrome {
301
338
  this.destroyed = false;
302
339
  this.probe = null;
303
340
  this.roving = null;
341
+ this.remote = false;
304
342
 
305
343
  this.dock = el("div", "page-dock", { role: "toolbar", "aria-label": "Formatting" });
306
344
  this.turn = el("button", "page-tool page-tool-turn page-tool-desk", { type: "button", "aria-haspopup": "menu" });
307
345
  this.more = el("button", "page-tool page-tool-primary page-tool-more", { type: "button", "aria-label": "More", title: "More", "aria-haspopup": "menu" });
308
346
  this.more.innerHTML = ICONS.more;
347
+ this.settingsBtn = el("button", "page-tool page-tool-desk page-tool-settings", { type: "button", "aria-label": "Document settings", title: "Document settings", "aria-haspopup": "dialog", "aria-expanded": "false" });
348
+ this.settingsBtn.innerHTML = ICONS.gear;
349
+ this.settingsEl = null;
350
+ this.settingsOpen = false;
351
+ this.settingsAnchor = null;
352
+ this.settingSwitches = {};
309
353
  this.dock.append(this.turn, el("span", "page-tool-sep page-tool-desk"), ...TOOLS.map(([n, l]) => toolButton(n, l)), this.more);
354
+ // The loader publishes the page facade when its module arrives, which can be after
355
+ // this toolbar is built, so the gear asks for the settings each time it is drawn.
356
+ this.dock.insertBefore(this.settingsBtn, this.more);
357
+ this.settingsBtn.hidden = !this.settingsAPI;
310
358
  this.plus = el("button", "page-plus", { type: "button", "aria-label": "Add a block", title: "Add a block" });
311
359
  this.plus.innerHTML = ICONS.plus;
312
360
  this.grip = el("button", "page-grip", { type: "button", "aria-label": "Block actions", title: "Drag to move, click for actions", "aria-haspopup": "menu" });
313
361
  this.grip.innerHTML = ICONS.grip;
314
362
  this.bubble = el("div", "page-bubble", { role: "toolbar", "aria-label": "Selection", hidden: "" });
315
- this.bubble.append(...TOOLS.map(([n, l]) => toolButton(n, l)));
363
+ this.bubbleTurn = el("button", "page-tool page-tool-turn", { type: "button", "aria-haspopup": "menu" });
364
+ this.bubbleMore = el("button", "page-tool page-tool-primary page-tool-more", { type: "button", "aria-label": "More", title: "More", "aria-haspopup": "menu" });
365
+ this.bubbleMore.innerHTML = ICONS.more;
366
+ this.bubble.append(this.bubbleTurn, el("span", "page-tool-sep"), ...TOOLS.map(([n, l]) => toolButton(n, l)), this.bubbleMore);
316
367
  this.menuId = `page-menu-${++menuCount}`;
317
368
  this.menuEl = el("div", "page-menu", { role: "menu", id: this.menuId, hidden: "" });
318
369
  this.scrim = el("div", "page-scrim", { hidden: "" });
@@ -329,13 +380,23 @@ class Chrome {
329
380
  }
330
381
  this.turn.addEventListener("click", () => this.toggleMenu("turn", this.turn));
331
382
  this.more.addEventListener("click", () => this.toggleMenu("more", this.more));
383
+ this.settingsBtn.addEventListener("click", () => this.toggleSettings());
384
+ this.bubbleTurn.addEventListener("click", () => this.toggleMenu("turn", this.bubbleTurn));
385
+ this.bubbleMore.addEventListener("click", () => this.toggleMenu("more", this.bubbleMore));
332
386
  this.dock.addEventListener("keydown", (e) => this.dockKey(e));
333
387
  this.plus.addEventListener("click", () => this.fromPlus());
334
388
  this.grip.addEventListener("click", (e) => {
335
389
  if (e.detail === 0) this.gripAction(this.gutterIndex());
336
390
  });
337
391
  this.drag = new BlockDrag(view, this.grip, { index: () => this.gutterIndex(), onClick: (i) => this.gripAction(i) });
338
- this.scrim.addEventListener("click", () => this.closeMenu());
392
+ // A tap that opens a menu ends in a click over the scrim it just showed: only a press
393
+ // that began on the scrim closes it.
394
+ let scrimPressed = false;
395
+ this.scrim.addEventListener("pointerdown", () => { scrimPressed = true; });
396
+ this.scrim.addEventListener("click", () => {
397
+ if (scrimPressed) this.closeMenu();
398
+ scrimPressed = false;
399
+ });
339
400
  this.menuEl.addEventListener("click", (e) => {
340
401
  const row = e.target.closest(".page-menu-item");
341
402
  if (row) this.pick(Number(row.dataset.index));
@@ -356,13 +417,21 @@ class Chrome {
356
417
  this.setHover(null);
357
418
  };
358
419
  this.onLeave = () => { this.pendingHover = null; this.setHover(null); };
359
- this.onResize = () => this.scheduleLayout();
420
+ this.onResize = () => { this.scheduleLayout(); this.placeSettings(); };
360
421
  this.onDocDown = (e) => {
422
+ if (this.settingsOpen && ![this.settingsEl, this.settingsAnchor].some((part) => part?.contains(e.target))) this.closeSettings();
361
423
  if (!this.menu) return;
362
- if ([this.menuEl, this.plus, this.grip, this.turn].some((part) => part.contains(e.target))) return;
424
+ if ([this.menuEl, this.plus, this.grip, this.turn, this.bubbleTurn, this.bubbleMore].some((part) => part.contains(e.target))) return;
363
425
  this.closeMenu();
364
426
  };
427
+ this.onSettingsKey = (e) => {
428
+ if (e.key !== "Escape" || !this.settingsOpen) return;
429
+ e.preventDefault();
430
+ e.stopPropagation();
431
+ this.closeSettings(true);
432
+ };
365
433
  document.addEventListener("mousedown", this.onDocDown, true);
434
+ document.addEventListener("keydown", this.onSettingsKey, true);
366
435
  this.shell.addEventListener("mousemove", this.onMove);
367
436
  this.shell.addEventListener("mouseleave", this.onLeave);
368
437
  this.shell.addEventListener("keydown", this.onKey);
@@ -467,7 +536,7 @@ class Chrome {
467
536
  const m = this.menu;
468
537
  if (!m || m.slashAt !== null) return this.view.dom;
469
538
  const a = m.anchor;
470
- return a && a !== this.view.dom && (a.tagName === "BUTTON" || a.hasAttribute("tabindex")) ? a : this.view.dom;
539
+ return a && a !== this.view.dom && a.isConnected && (a.tagName === "BUTTON" || a.hasAttribute("tabindex")) ? a : this.view.dom;
471
540
  }
472
541
 
473
542
  // A block action from the phone menu acts on the caret's block when nothing
@@ -522,13 +591,116 @@ class Chrome {
522
591
  this.openMenu("insert", this.plus, null, null, this.view.state.doc.child(i).attrs.id);
523
592
  }
524
593
 
594
+ // The gear's panel: built once, filled from the document's own settings, and
595
+ // placed under the control that opened it.
596
+ buildSettings() {
597
+ if (this.settingsEl) return this.settingsEl;
598
+ const panel = el("div", "page-settings", { role: "dialog", "aria-label": "Document settings", hidden: "" });
599
+ const head = el("div", "page-settings-head");
600
+ const title = el("span", "page-settings-title");
601
+ title.textContent = "Document settings";
602
+ const close = el("button", "page-settings-close", { type: "button", "aria-label": "Close settings" });
603
+ close.innerHTML = ICONS.close;
604
+ close.addEventListener("click", () => this.closeSettings(true));
605
+ head.append(title, close);
606
+ const rows = SETTING_ROWS.map(([key, label]) => {
607
+ const row = el("div", "page-settings-row");
608
+ const name = el("span", "page-settings-name", { id: `${this.menuId}-set-${key}` });
609
+ name.textContent = label;
610
+ const sw = el("button", "page-settings-switch", { type: "button", role: "switch", "aria-checked": "true", "aria-labelledby": name.id, "data-setting": key });
611
+ sw.append(el("span", "page-settings-knob"));
612
+ sw.addEventListener("click", () => this.flipSetting(key));
613
+ this.settingSwitches[key] = sw;
614
+ row.append(name, sw);
615
+ return row;
616
+ });
617
+ const note = el("div", "page-settings-note");
618
+ note.textContent = "Hidden values stay saved and come back when turned on.";
619
+ panel.append(head, ...rows, note);
620
+ this.settingsEl = panel;
621
+ this.shell.append(panel);
622
+ return panel;
623
+ }
624
+
625
+ flipSetting(key) {
626
+ const sw = this.settingSwitches[key];
627
+ const on = sw.getAttribute("aria-checked") !== "true";
628
+ sw.setAttribute("aria-checked", String(on));
629
+ Promise.resolve().then(() => this.settingsAPI.set(key, on)).catch(() => {
630
+ sw.setAttribute("aria-checked", String(!on));
631
+ toast("That setting could not be saved.", "error");
632
+ });
633
+ }
634
+
635
+ openSettings(anchor = this.settingsBtn) {
636
+ if (!this.settingsAPI) return;
637
+ if (this.menu) this.closeMenu();
638
+ const panel = this.buildSettings();
639
+ const now = this.settingsAPI.read();
640
+ for (const [key, sw] of Object.entries(this.settingSwitches)) sw.setAttribute("aria-checked", String(now[key] !== false));
641
+ this.settingsOpen = true;
642
+ this.settingsAnchor = anchor;
643
+ anchor.setAttribute("aria-expanded", "true");
644
+ panel.hidden = false;
645
+ this.placeSettings();
646
+ panel.querySelector(".page-settings-close")?.focus({ preventScroll: true });
647
+ }
648
+
649
+ closeSettings(restoreFocus = false) {
650
+ if (!this.settingsOpen) return;
651
+ const anchor = this.settingsAnchor;
652
+ this.settingsOpen = false;
653
+ this.settingsAnchor = null;
654
+ if (this.settingsEl) this.settingsEl.hidden = true;
655
+ anchor?.setAttribute("aria-expanded", "false");
656
+ if (restoreFocus && anchor?.isConnected) anchor.focus({ preventScroll: true });
657
+ }
658
+
659
+ toggleSettings() {
660
+ if (this.settingsOpen) this.closeSettings(true);
661
+ else this.openSettings(this.settingsBtn);
662
+ }
663
+
664
+ // The panel's right edge sits on its control's right edge, under it, and it
665
+ // flips above the control when there is no room below.
666
+ placeSettings() {
667
+ const panel = this.settingsEl;
668
+ if (!panel || panel.hidden || !this.settingsAnchor) return;
669
+ const r = this.settingsAnchor.getBoundingClientRect();
670
+ const width = panel.offsetWidth || 264;
671
+ const height = panel.offsetHeight || 0;
672
+ if (this.isPhone()) {
673
+ const v = phoneViewport(this.win);
674
+ const x = Math.max(v.left + PHONE_INSET, Math.min(r.right - width, v.left + v.width - PHONE_INSET - width));
675
+ let y = r.top - 6 - height;
676
+ if (y < v.top + PHONE_INSET) y = Math.min(r.bottom + 6, v.top + v.height - PHONE_INSET - height);
677
+ panel.style.setProperty("--page-pop-x", `${x}px`);
678
+ panel.style.setProperty("--page-pop-y", `${Math.max(v.top + PHONE_INSET, y)}px`);
679
+ return;
680
+ }
681
+ const shellBox = this.shell.getBoundingClientRect();
682
+ const x = Math.max(8, Math.min(r.right - shellBox.left - width, Math.max(8, shellBox.width - width - 8)));
683
+ let y = r.bottom - shellBox.top + 6;
684
+ if (y + height > this.win.innerHeight - 8 && r.top - shellBox.top - 6 - height >= 8) y = r.top - shellBox.top - 6 - height;
685
+ panel.style.setProperty("--page-pop-x", `${x}px`);
686
+ panel.style.setProperty("--page-pop-y", `${y}px`);
687
+ }
688
+
525
689
  toggleMenu(mode, anchor) {
526
690
  if (this.menu) this.closeMenu();
527
691
  else this.openMenu(mode, anchor);
528
692
  }
529
693
 
694
+ // A slash dismissed with Escape stays text: it is known by its block and the text
695
+ // before it, so typing after it does not bring the menu back.
696
+ slashKey(slashAt) {
697
+ const $at = this.view.state.doc.resolve(slashAt);
698
+ return `${$at.parent.attrs.id}\u0000${this.view.state.doc.textBetween($at.start(), slashAt - 1)}`;
699
+ }
700
+
530
701
  openMenu(mode, anchor, slashAt = null, ids = null, anchorId = null) {
531
- this.menu = { mode, anchor, slashAt, anchorId, ids: ids ?? (mode === "insert" ? [] : this.selectionIds()), query: "", active: 0, items: [] };
702
+ this.closeSettings();
703
+ this.menu = { mode, anchor, slashAt, anchorId, ids: ids ?? (mode === "insert" ? [] : this.selectionIds()), query: "", active: 0, items: [], persist: false };
532
704
  anchor.setAttribute("aria-expanded", "true");
533
705
  if (slashAt !== null) {
534
706
  if (!this.priorAria) this.priorAria = POPUP_ATTRS.map((name) => [name, this.view.dom.getAttribute(name)]);
@@ -555,14 +727,23 @@ class Chrome {
555
727
  // Everything the phone toolbar folds away: the rest of the formatting, and the
556
728
  // block actions the desktop menu carries. The move commands are the same ones.
557
729
  moreItems() {
558
- const tools = TOOLS.filter(([name]) => !PHONE_TOOLS.has(name))
559
- .map(([name, label]) => ({ key: name, label, icon: ICONS[name], run: (view) => runMark(view, name) }));
730
+ const phone = this.isPhone();
731
+ const tools = phone ? TOOLS.filter(([name]) => !PHONE_TOOLS.has(name))
732
+ .map(([name, label]) => ({ key: name, label, icon: name, run: (view) => runMark(view, name) })) : [];
560
733
  const actions = ACTION_ITEMS.filter((a) => MORE_ACTIONS.has(a.key)).map((a) => ({ ...a, blocks: true }));
561
734
  return [
562
735
  ...tools,
563
- { key: "turn", label: "Turn into", icon: "\u21bb", run: (view, chrome) => chrome.openMenu("turn", chrome.more) },
564
- { key: "insert", label: "Insert", icon: "+", run: (view, chrome) => chrome.openMenu("insert", chrome.more, null, null, view.state.doc.child(chrome.blockIndex()).attrs.id) },
736
+ { key: "underline", label: "Underline", icon: "U", hint: `${MOD}u`, run: (view, chrome) => nativeMenu(view, chrome, "underline") },
737
+ { key: "text_color", label: "Text color", icon: "A", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "text_color") },
738
+ { key: "highlight", label: "Highlight", icon: "\u25A0", hint: "\u203a", run: (view, chrome) => nativeMenu(view, chrome, "highlight") },
739
+ ...(phone ? [{ key: "turn", label: "Turn into", icon: "turn", run: (view, chrome) => chrome.openMenu("turn", chrome.more) }] : []),
740
+ { key: "insert", label: "Insert", icon: "+", run: (view, chrome) => chrome.openMenu("insert", chrome.pickedFrom ?? chrome.more, null, null, view.state.doc.child(chrome.blockIndex()).attrs.id) },
565
741
  ...actions,
742
+ ...(phone && this.settingsAPI ? [{ key: "settings", label: "Document settings", icon: "gear", focus: false, run: (view, chrome) => chrome.openSettings(chrome.more) }] : []),
743
+ { key: "titleIcon", label: "Page icon", icon: "\u263A", run: (view) => runMenuCommand(view, "titleIcon") },
744
+ { key: "titleCover", label: "Add cover", icon: "\u25A4", run: (view) => runMenuCommand(view, "titleCover") },
745
+ { key: "exportHtml", label: "Export reading copy", icon: "\u21E9", run: (view) => runMenuCommand(view, "exportHtml") },
746
+ { key: "exportMarkdown", label: "Export as Markdown", icon: "M\u21E9", run: (view) => runMenuCommand(view, "exportMarkdown") },
566
747
  ];
567
748
  }
568
749
 
@@ -619,7 +800,8 @@ class Chrome {
619
800
  const role = m.mode === "turn" ? "menuitemradio" : typeof checked === "boolean" ? "menuitemcheckbox" : slash ? "option" : "menuitem";
620
801
  const row = el("button", `page-menu-item${active ? " is-active" : ""}`, { type: "button", role, tabindex: !slash && active ? "0" : "-1", id: `${this.menuId}-${i}`, "data-index": String(i), ...(role === "menuitemradio" ? {} : { "aria-selected": active ? "true" : "false" }), ...(typeof checked === "boolean" ? { "aria-checked": String(checked) } : {}) });
621
802
  const icon = el("span", "page-menu-icon");
622
- icon.textContent = b.icon || "";
803
+ if (ICONS[b.icon]) icon.innerHTML = ICONS[b.icon];
804
+ else icon.textContent = b.icon || "";
623
805
  const label = el("span", "page-menu-label");
624
806
  label.textContent = b.label;
625
807
  if (b.description) {
@@ -642,7 +824,7 @@ class Chrome {
642
824
  this.menuEl.hidden = false;
643
825
  this.scrim.hidden = false;
644
826
  this.scheduleLayout();
645
- if (!slash) this.menuEl.querySelectorAll("button")[m.active]?.focus();
827
+ if (!slash) this.menuEl.querySelectorAll("button")[m.active]?.focus({ preventScroll: true });
646
828
  }
647
829
 
648
830
  pick(i) {
@@ -656,6 +838,8 @@ class Chrome {
656
838
  const wanted = m.mode === "insert" ? (m.anchorId ? [m.anchorId] : []) : m.ids;
657
839
  const stale = wanted.some((id) => !id || this.idIndex(id) < 0);
658
840
  const index = wanted.length ? this.idIndex(wanted[0]) : -1;
841
+ // A submenu a row opens (text color, highlight) anchors where this menu was opened.
842
+ this.pickedFrom = m.anchor;
659
843
  this.closeMenu();
660
844
  if (stale && m.mode !== "external") {
661
845
  announce(this.view, TARGET_GONE);
@@ -668,10 +852,7 @@ class Chrome {
668
852
  this.view.focus();
669
853
  return;
670
854
  }
671
- const tr = st.tr;
672
- for (const id of wanted) retype(tr, this.idIndex(id), item);
673
- this.view.dispatch(asStep(tr).scrollIntoView());
674
- this.view.focus();
855
+ turnBlocks(this.view, item, wanted);
675
856
  } else if (m.mode === "actions" || m.mode === "more" || m.mode === "external") {
676
857
  if (item.blocks) this.selectCaretBlock(wanted);
677
858
  item.run(this.view, this, index >= 0 ? index : this.blockIndex());
@@ -709,8 +890,13 @@ class Chrome {
709
890
  if (m.items.length) this.pick(m.active);
710
891
  return true;
711
892
  }
893
+ if (event.key === "Tab" && m.slashAt === null) {
894
+ this.closeMenu();
895
+ return false;
896
+ }
712
897
  if (event.key === "Escape") {
713
898
  const target = this.invoker();
899
+ if (m.slashAt !== null) this.dismissedSlash = this.slashKey(m.slashAt);
714
900
  this.closeMenu();
715
901
  if (target === this.view.dom) this.view.focus();
716
902
  else target?.focus?.();
@@ -818,6 +1004,8 @@ class Chrome {
818
1004
  const r = m.anchor === this.plus ? (dom?.getBoundingClientRect?.() ?? edBox) : m.anchor.getBoundingClientRect();
819
1005
  left = r.left;
820
1006
  top = r.bottom + 6;
1007
+ const height = this.menuEl.offsetHeight;
1008
+ if (top + height > this.win.innerHeight - 8 && r.top - 6 - height >= 8) top = r.top - 6 - height;
821
1009
  }
822
1010
  out.menu = { x: Math.max(8, Math.min(left - shellBox.left, shellBox.width - 280)), y: top - shellBox.top };
823
1011
  }
@@ -856,22 +1044,46 @@ class Chrome {
856
1044
  }
857
1045
  }
858
1046
 
1047
+ get settingsAPI() {
1048
+ return globalThis.clay?.page?.settings ?? null;
1049
+ }
1050
+
859
1051
  update(view, prev) {
1052
+ const settingsHidden = !this.settingsAPI;
1053
+ if (this.settingsBtn.hidden !== settingsHidden) this.settingsBtn.hidden = settingsHidden;
860
1054
  this.view = view;
861
1055
  if (this.menu && this.menu.slashAt === null && prev && prev.doc !== view.state.doc) {
862
- const hadFocus = this.menuEl.contains(document.activeElement);
863
- this.closeMenu();
864
- if (hadFocus) view.focus();
1056
+ // A menu whose rows resolve their targets by id, from a control the page keeps,
1057
+ // is still the right menu after a peer's edit.
1058
+ if (this.menu.persist && this.menu.anchor?.isConnected) this.scheduleLayout();
1059
+ else {
1060
+ const hadFocus = this.menuEl.contains(document.activeElement);
1061
+ this.closeMenu();
1062
+ if (hadFocus) view.focus();
1063
+ }
865
1064
  }
866
1065
  const st = view.state;
867
1066
  const node = st.doc.child(this.blockIndex());
868
1067
  this.turn.textContent = `${blockTypeOf(node).label} ▾`;
1068
+ this.bubbleTurn.textContent = this.turn.textContent;
869
1069
  for (const b of this.dock.querySelectorAll("[data-cmd]")) b.setAttribute("aria-pressed", String(toolActive(st, b.dataset.cmd)));
870
- const showBubble = !st.selection.empty && st.selection instanceof TextSelection && view.hasFocus();
1070
+ const bubbleMenu = !!this.menu && [this.bubbleTurn, this.bubbleMore].includes(this.menu.anchor);
1071
+ const showBubble = bubbleMenu || (!st.selection.empty && st.selection instanceof TextSelection && view.hasFocus());
871
1072
  this.bubble.hidden = !showBubble;
872
1073
  if (showBubble) for (const b of this.bubble.querySelectorAll("[data-cmd]")) b.setAttribute("aria-pressed", String(toolActive(st, b.dataset.cmd)));
873
1074
  this.grip.classList.toggle("is-on", st.selection instanceof BlockSelection);
874
1075
  if (!this.inRoving(this.roving)) this.setRoving(this.dockControls()[0]);
1076
+ // A slash word typed before the caret left the block reopens when its own word is
1077
+ // edited again, not when the caret merely returns to it.
1078
+ if (!this.menu && prev && prev.doc !== st.doc && !this.remote && view.hasFocus() && st.selection.empty) {
1079
+ const $head = st.selection.$head;
1080
+ if ($head.depth === 1 && $head.parent.isTextblock && !$head.parent.type.spec.code) {
1081
+ const before = st.doc.textBetween($head.start(), $head.pos);
1082
+ const match = /(?:^|\s)\/([a-z]*)$/i.exec(before);
1083
+ const slashAt = match ? $head.start() + match.index + match[0].indexOf("/") + 1 : null;
1084
+ if (match && this.slashKey(slashAt) !== this.dismissedSlash) this.openMenu("insert", this.plus, slashAt, null, $head.parent.attrs.id);
1085
+ }
1086
+ }
875
1087
  this.slashQuery();
876
1088
  this.scheduleLayout();
877
1089
  }
@@ -890,7 +1102,8 @@ class Chrome {
890
1102
  this.win.visualViewport?.removeEventListener("resize", this.onResize);
891
1103
  this.win.visualViewport?.removeEventListener("scroll", this.onResize);
892
1104
  document.removeEventListener("mousedown", this.onDocDown, true);
893
- for (const part of [this.dock, this.plus, this.grip, this.bubble, this.scrim, this.menuEl, this.probe]) part?.remove();
1105
+ document.removeEventListener("keydown", this.onSettingsKey, true);
1106
+ for (const part of [this.dock, this.plus, this.grip, this.bubble, this.scrim, this.menuEl, this.settingsEl, this.probe]) part?.remove();
894
1107
  }
895
1108
  }
896
1109
 
@@ -903,6 +1116,10 @@ export function chromePlugin({ onAiTool = null } = {}) {
903
1116
  chromeFor.set(view, chrome);
904
1117
  return chrome;
905
1118
  },
1119
+ filterTransaction(tr) {
1120
+ if (chrome && tr.docChanged) chrome.remote = !!tr.getMeta(REMOTE);
1121
+ return true;
1122
+ },
906
1123
  props: {
907
1124
  handleKeyDown(view, event) {
908
1125
  if (!chrome) return false;
@@ -928,7 +1145,7 @@ export function chromePlugin({ onAiTool = null } = {}) {
928
1145
  handleDOMEvents: {
929
1146
  keydown(view, event) { return composing(event, view); },
930
1147
  blur(view) {
931
- if (chrome) chrome.bubble.hidden = true;
1148
+ if (chrome && !(chrome.menu && [chrome.bubbleTurn, chrome.bubbleMore].includes(chrome.menu.anchor))) chrome.bubble.hidden = true;
932
1149
  return false;
933
1150
  },
934
1151
  },
@@ -0,0 +1,126 @@
1
+ // Lists and tables on the clipboard in the saved article's shape, so another app
2
+ // gets real lists and a header row and a Document reads them back as the same
3
+ // blocks. A copy nests list items as <ul>/<ol> by indent, leads a check item with
4
+ // its checkbox and names a table's columns in a <thead>; a toggle's own list items
5
+ // keep the editor's flat form, which carries their toggle. On paste,
6
+ // flattenPastedHTML turns the lists back into items and moves the slice's open
7
+ // depths (data-pm-slice, left on the dropped <ul>) to the first block, and the
8
+ // table rule in blocks.js reads a table through table() and rows() below; a
9
+ // Document's own table skips pasteTable's grid path for it. The text flavor is
10
+ // markdown list lines (parseListText reads them) and tab-separated table rows.
11
+ // blocks.js loads this module when an editor mounts, off the input-ready path.
12
+ import { model } from "../vendor/prosemirror.vendor.js";
13
+ import { schema } from "./schema.js";
14
+ import { keysBetween, newId, orderedIds } from "./model.js";
15
+ import { clipboardParser } from "./blocks.js";
16
+
17
+ const N = schema.nodes;
18
+ const parentOf = (n) => n.attrs.parent ?? null;
19
+
20
+ const copiedCols = (table) => {
21
+ const used = new Set();
22
+ table.forEach((row) => row.forEach((cell) => used.add(cell.attrs.col)));
23
+ return orderedIds(table.attrs.cols).filter((id) => used.has(id));
24
+ };
25
+
26
+ function tableHead(table, dom) {
27
+ const doc = dom.ownerDocument;
28
+ const tr = doc.createElement("thead").appendChild(doc.createElement("tr"));
29
+ for (const id of copiedCols(table)) {
30
+ const col = table.attrs.cols[id], th = tr.appendChild(doc.createElement("th"));
31
+ th.setAttribute("data-col", id);
32
+ th.setAttribute("scope", "col");
33
+ if (col.kind) th.setAttribute("data-kind", col.kind);
34
+ if (col.width) { th.setAttribute("data-width", String(col.width)); th.style.width = `${col.width}px`; }
35
+ th.textContent = col.name ?? "";
36
+ }
37
+ dom.prepend(tr.parentNode);
38
+ if (table.attrs.sort) dom.setAttribute("data-sort", JSON.stringify(table.attrs.sort));
39
+ }
40
+
41
+ // The serializer's top-level elements, one per block of the fragment, rewritten in place.
42
+ export function copy(fragment, doms) {
43
+ let lists = [];
44
+ fragment.forEach((n, _, i) => {
45
+ const dom = doms[i];
46
+ if (!dom || dom.nodeType !== 1) return;
47
+ if (n.type === N.table) tableHead(n, dom);
48
+ if (n.type !== N.list_item || parentOf(n) !== null) { lists = []; return; }
49
+ const doc = dom.ownerDocument, { indent, kind } = n.attrs, tag = kind === "ordered" ? "OL" : "UL";
50
+ while (lists.length && lists.at(-1).indent > indent) lists.pop();
51
+ if (lists.at(-1)?.indent === indent && lists.at(-1).list.tagName !== tag) lists.pop();
52
+ if (!lists.length || lists.at(-1).indent < indent) {
53
+ const list = doc.createElement(tag);
54
+ if (lists.length) lists.at(-1).li.append(list); else dom.before(list);
55
+ lists.push({ indent, list, li: null });
56
+ }
57
+ const li = doc.createElement("li");
58
+ if (kind === "check") li.innerHTML = `<input type="checkbox" disabled${n.attrs.checked ? " checked" : ""}>`;
59
+ li.append(...dom.childNodes);
60
+ lists.at(-1).list.append(li);
61
+ lists.at(-1).li = li;
62
+ dom.remove();
63
+ });
64
+ }
65
+
66
+ const quoteCell = (text) => (/[\t\n\r"]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text);
67
+
68
+ export function text(slice) {
69
+ if (!slice.content.firstChild?.type.spec.group?.includes("block")) return null;
70
+ let out = "", runs = [], prevList = false;
71
+ slice.content.forEach((n, _, i) => {
72
+ const list = n.type === N.list_item;
73
+ let line = n.textBetween(0, n.content.size, "\n\n");
74
+ if (list) {
75
+ const { indent, kind } = n.attrs;
76
+ while (runs.length && runs.at(-1).indent > indent) runs.pop();
77
+ if (runs.at(-1)?.indent === indent && runs.at(-1).kind !== kind) runs.pop();
78
+ if (!runs.length || runs.at(-1).indent < indent) runs.push({ indent, kind, n: 0 });
79
+ const run = runs.at(-1);
80
+ const marker = kind === "ordered" ? `${++run.n}.` : kind === "check" ? `- [${n.attrs.checked ? "x" : " "}]` : "-";
81
+ line = `${" ".repeat(runs.length - 1)}${marker} ${line}`;
82
+ } else runs = [];
83
+ if (n.type === N.table) {
84
+ const rows = [copiedCols(n).map((id) => quoteCell(n.attrs.cols[id].name ?? ""))];
85
+ n.forEach((row) => { const cells = []; row.forEach((cell) => cells.push(quoteCell(cell.textContent))); rows.push(cells); });
86
+ line = rows.map((row) => row.join("\t")).join("\n");
87
+ }
88
+ out += (i ? (prevList && list ? "\n" : "\n\n") : "") + line;
89
+ prevList = list;
90
+ });
91
+ return out;
92
+ }
93
+
94
+ // A pasted table: its header row (a <thead> row, or a first row of <th>) names
95
+ // the columns, and a copied one keeps each column's id, kind and width; every
96
+ // other row is a row of cells. Rows get their ids and order keys from identityPlugin.
97
+ const tables = new WeakMap();
98
+ function pastedTable(dom) {
99
+ if (tables.has(dom)) return tables.get(dom);
100
+ const cellsOf = (tr) => [...tr.children].filter((c) => c.tagName === "TD" || c.tagName === "TH");
101
+ const trs = [...dom.querySelectorAll("tr")].filter((tr) => tr.closest("table") === dom);
102
+ const head = trs.find((tr) => tr.parentElement.tagName === "THEAD") ?? (trs.length > 1 && cellsOf(trs[0]).every((c) => c.tagName === "TH") ? trs[0] : null);
103
+ const body = trs.filter((tr) => tr !== head);
104
+ const names = head ? cellsOf(head) : [];
105
+ const width = Math.max(1, names.length, ...body.map((tr) => cellsOf(tr).length));
106
+ const keys = keysBetween(null, null, width);
107
+ const ids = [], cols = {};
108
+ for (let i = 0; i < width; i++) {
109
+ const th = names[i];
110
+ let id = th?.getAttribute("data-col");
111
+ if (!id || Object.hasOwn(cols, id)) id = newId("c");
112
+ const kind = th?.getAttribute("data-kind"), w = Math.round(Number(th?.getAttribute("data-width")));
113
+ cols[id] = { pos: keys[i], name: th?.textContent.replace(/\s+/g, " ").trim() || `Column ${i + 1}`, ...(kind ? { kind } : {}), ...(w > 0 ? { width: w } : {}) };
114
+ ids.push(id);
115
+ }
116
+ const cell = (td, col) => (td ? clipboardParser.parse(td, { topNode: N.table_cell.create({ col }) }) : N.table_cell.create({ col }));
117
+ const rows = (body.length ? body : [null]).map((tr) => N.table_row.create(null, ids.map((col, i) => cell(tr && cellsOf(tr)[i], col))));
118
+ let sort = null;
119
+ try { sort = JSON.parse(dom.getAttribute("data-sort")); } catch {}
120
+ const found = { cols, sort: sort && ids.includes(sort.col) && (sort.dir === "asc" || sort.dir === "desc") ? { col: sort.col, dir: sort.dir } : null, rows: model.Fragment.from(rows) };
121
+ tables.set(dom, found);
122
+ return found;
123
+ }
124
+
125
+ export const table = (dom) => ({ cols: pastedTable(dom).cols, sort: pastedTable(dom).sort, parent: dom.getAttribute("data-page-parent") || null });
126
+ export const rows = (dom) => pastedTable(dom).rows;