@threadlabs/looma 0.4.0 → 0.5.1

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 (491) hide show
  1. package/README.md +43 -12
  2. package/components/shared/editor.js +69 -0
  3. package/components/shared/input-modality.d.ts +8 -0
  4. package/components/shared/input-modality.js +22 -0
  5. package/components/shared/overlay.js +281 -0
  6. package/components/shared/trigger.js +17 -0
  7. package/components/ui-affordance-scope/ui-affordance-scope.html +22 -0
  8. package/components/ui-affordance-scope/ui-affordance-scope.js +76 -0
  9. package/components/ui-avatar/ui-avatar.html +59 -0
  10. package/components/ui-avatar/ui-avatar.js +47 -0
  11. package/components/ui-avatar-group/ui-avatar-group.html +62 -0
  12. package/components/ui-avatar-group/ui-avatar-group.js +27 -0
  13. package/components/ui-badge/ui-badge.html +94 -0
  14. package/components/ui-button/ui-button.html +226 -0
  15. package/components/ui-callout/ui-callout.html +104 -0
  16. package/components/ui-checkbox/ui-checkbox.html +107 -0
  17. package/components/ui-checkbox/ui-checkbox.js +24 -0
  18. package/components/ui-cluster/ui-cluster.html +49 -0
  19. package/components/ui-combobox/ui-combobox.html +377 -0
  20. package/components/ui-combobox/ui-combobox.js +432 -0
  21. package/components/ui-container/ui-container.html +39 -0
  22. package/components/ui-context-menu/ui-context-menu.html +75 -0
  23. package/components/ui-context-menu/ui-context-menu.js +143 -0
  24. package/components/ui-dialog/ui-dialog.html +178 -0
  25. package/components/ui-dialog/ui-dialog.js +105 -0
  26. package/components/ui-disclosure/ui-disclosure.html +109 -0
  27. package/components/ui-disclosure/ui-disclosure.js +28 -0
  28. package/components/ui-editable/ui-editable.html +163 -0
  29. package/components/ui-editable/ui-editable.js +113 -0
  30. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.html +97 -0
  31. package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.js +71 -0
  32. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +157 -0
  33. package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +50 -0
  34. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +197 -0
  35. package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +44 -0
  36. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +164 -0
  37. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +75 -0
  38. package/components/ui-editor-table-overlay/ui-editor-table-overlay.html +422 -0
  39. package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +119 -0
  40. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +271 -0
  41. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +54 -0
  42. package/components/ui-editor-toolbar/ui-editor-toolbar.html +110 -0
  43. package/components/ui-floating-action-button/ui-floating-action-button.html +83 -0
  44. package/components/ui-form-field/ui-form-field.html +45 -0
  45. package/components/ui-form-field/ui-form-field.js +71 -0
  46. package/components/ui-grid/ui-grid.html +46 -0
  47. package/components/ui-icon/ui-icon.html +34 -0
  48. package/components/ui-icon/ui-icon.js +8 -0
  49. package/components/ui-icon-button/ui-icon-button.html +180 -0
  50. package/components/ui-icon-button/ui-icon-button.js +7 -0
  51. package/components/ui-input/ui-input.html +81 -0
  52. package/components/ui-input/ui-input.js +7 -0
  53. package/components/ui-menu/ui-menu.html +87 -0
  54. package/components/ui-menu/ui-menu.js +114 -0
  55. package/components/ui-menu-item/ui-menu-item.html +37 -0
  56. package/components/ui-popover/ui-popover.html +85 -0
  57. package/components/ui-popover/ui-popover.js +70 -0
  58. package/components/ui-radio/ui-radio.html +57 -0
  59. package/components/ui-radio/ui-radio.js +25 -0
  60. package/components/ui-radio-group/ui-radio-group.html +51 -0
  61. package/components/ui-radio-group/ui-radio-group.js +42 -0
  62. package/components/ui-reel/ui-reel.html +67 -0
  63. package/components/ui-search-result-row/ui-search-result-row.html +101 -0
  64. package/components/ui-search-shell/ui-search-shell.html +142 -0
  65. package/components/ui-search-shell/ui-search-shell.js +60 -0
  66. package/components/ui-select/ui-select.html +87 -0
  67. package/components/ui-select/ui-select.js +16 -0
  68. package/components/ui-separator/ui-separator.html +22 -0
  69. package/components/ui-sidebar/ui-sidebar.html +183 -0
  70. package/components/ui-sidebar/ui-sidebar.js +178 -0
  71. package/components/ui-stack/ui-stack.html +59 -0
  72. package/components/ui-switch/ui-switch.html +95 -0
  73. package/components/ui-switch/ui-switch.js +24 -0
  74. package/components/ui-switcher/ui-switcher.html +65 -0
  75. package/components/ui-tabs/ui-tabs.html +134 -0
  76. package/components/ui-tabs/ui-tabs.js +81 -0
  77. package/components/ui-textarea/ui-textarea.html +80 -0
  78. package/components/ui-textarea/ui-textarea.js +7 -0
  79. package/components/ui-toast-region/ui-toast-region.html +127 -0
  80. package/components/ui-toast-region/ui-toast-region.js +119 -0
  81. package/components/ui-tooltip/ui-tooltip.html +122 -0
  82. package/components/ui-tooltip/ui-tooltip.js +134 -0
  83. package/components/ui-top-bar/ui-top-bar.html +53 -0
  84. package/components/ui-tree/ui-tree.html +29 -0
  85. package/components/ui-tree/ui-tree.js +250 -0
  86. package/components/ui-tree-item/ui-tree-item.html +326 -0
  87. package/components/ui-tree-item/ui-tree-item.js +94 -0
  88. package/dist/index.d.ts +4 -414
  89. package/dist/index.js +55 -657
  90. package/editor/chunks/extensions.js +7267 -0
  91. package/editor/chunks/table-backgrounds.js +51 -0
  92. package/editor/chunks/ui.js +92 -0
  93. package/editor/extensions/callout.d.ts +20 -0
  94. package/editor/extensions/index.d.ts +12 -441
  95. package/editor/extensions/index.js +3 -64
  96. package/editor/extensions/list-behavior.d.ts +7 -0
  97. package/editor/extensions/mention.d.ts +35 -0
  98. package/editor/extensions/preset.d.ts +42 -0
  99. package/editor/extensions/slash-command.d.ts +80 -0
  100. package/editor/extensions/smart-paste.d.ts +13 -0
  101. package/editor/extensions/table-commands.d.ts +114 -0
  102. package/editor/extensions/table-formatting.d.ts +60 -0
  103. package/editor/icons.d.ts +55 -0
  104. package/editor/index.d.ts +8 -6
  105. package/editor/index.js +96 -71
  106. package/editor/insert-table-grid.d.ts +7 -0
  107. package/editor/mention-contract.d.ts +78 -0
  108. package/editor/mention-menu.d.ts +9 -0
  109. package/editor/slash-menu.d.ts +29 -0
  110. package/editor/table-backgrounds.d.ts +43 -0
  111. package/editor/table-context-menu.d.ts +12 -0
  112. package/editor/table-overlay.d.ts +79 -0
  113. package/editor/ui.d.ts +10 -50
  114. package/editor/ui.js +3 -14
  115. package/editor/viewport.d.ts +15 -0
  116. package/package.json +84 -60
  117. package/styles/ui-affordance-scope.css +19 -0
  118. package/styles/ui-avatar-group.css +56 -0
  119. package/styles/ui-avatar.css +47 -0
  120. package/styles/ui-badge.css +91 -0
  121. package/styles/ui-button.css +236 -0
  122. package/styles/ui-callout.css +76 -0
  123. package/styles/ui-checkbox.css +98 -0
  124. package/styles/ui-cluster.css +46 -0
  125. package/styles/ui-combobox.css +213 -0
  126. package/styles/ui-container.css +36 -0
  127. package/styles/ui-context-menu.css +68 -0
  128. package/styles/ui-dialog.css +160 -0
  129. package/styles/ui-disclosure.css +95 -0
  130. package/styles/ui-editable.css +139 -0
  131. package/styles/ui-editor-insert-table-grid.css +65 -0
  132. package/styles/ui-editor-mention-menu.css +121 -0
  133. package/styles/ui-editor-slash-menu.css +179 -0
  134. package/styles/ui-editor-table-context-menu.css +118 -0
  135. package/styles/ui-editor-table-overlay.css +318 -0
  136. package/styles/ui-editor-table-toolbar.css +191 -0
  137. package/styles/ui-editor-toolbar.css +115 -0
  138. package/styles/ui-floating-action-button.css +82 -0
  139. package/styles/ui-form-field.css +36 -0
  140. package/styles/ui-grid.css +43 -0
  141. package/styles/ui-icon-button.css +184 -0
  142. package/styles/ui-icon.css +23 -0
  143. package/styles/ui-input.css +82 -0
  144. package/styles/ui-menu-item.css +34 -0
  145. package/styles/ui-menu.css +80 -0
  146. package/styles/ui-popover.css +76 -0
  147. package/styles/ui-radio-group.css +38 -0
  148. package/styles/ui-radio.css +46 -0
  149. package/styles/ui-reel.css +63 -0
  150. package/styles/ui-search-result-row.css +92 -0
  151. package/styles/ui-search-shell.css +128 -0
  152. package/styles/ui-select.css +86 -0
  153. package/styles/ui-separator.css +16 -0
  154. package/styles/ui-sidebar.css +166 -0
  155. package/styles/ui-stack.css +55 -0
  156. package/styles/ui-switch.css +88 -0
  157. package/styles/ui-switcher.css +61 -0
  158. package/styles/ui-tabs.css +114 -0
  159. package/styles/ui-textarea.css +77 -0
  160. package/styles/ui-toast-region.css +103 -0
  161. package/styles/ui-tooltip.css +113 -0
  162. package/styles/ui-top-bar.css +51 -0
  163. package/styles/ui-tree-item.css +267 -0
  164. package/styles/ui-tree.css +16 -0
  165. package/vanilla/UiAffordanceScope.d.ts +14 -0
  166. package/vanilla/UiAffordanceScope.js +30 -0
  167. package/vanilla/UiAvatar.d.ts +17 -0
  168. package/vanilla/UiAvatar.js +41 -0
  169. package/vanilla/UiAvatarGroup.d.ts +15 -0
  170. package/vanilla/UiAvatarGroup.js +40 -0
  171. package/vanilla/UiBadge.d.ts +15 -0
  172. package/vanilla/UiBadge.js +30 -0
  173. package/vanilla/UiButton.d.ts +16 -0
  174. package/vanilla/UiButton.js +32 -0
  175. package/vanilla/UiCallout.d.ts +14 -0
  176. package/vanilla/UiCallout.js +124 -0
  177. package/vanilla/UiCheckbox.d.ts +19 -0
  178. package/vanilla/UiCheckbox.js +47 -0
  179. package/vanilla/UiCluster.d.ts +15 -0
  180. package/vanilla/UiCluster.js +30 -0
  181. package/vanilla/UiCombobox.d.ts +42 -0
  182. package/vanilla/UiCombobox.js +194 -0
  183. package/vanilla/UiContainer.d.ts +15 -0
  184. package/vanilla/UiContainer.js +30 -0
  185. package/vanilla/UiContextMenu.d.ts +18 -0
  186. package/vanilla/UiContextMenu.js +35 -0
  187. package/vanilla/UiDialog.d.ts +19 -0
  188. package/vanilla/UiDialog.js +53 -0
  189. package/vanilla/UiDisclosure.d.ts +18 -0
  190. package/vanilla/UiDisclosure.js +49 -0
  191. package/vanilla/UiEditable.d.ts +22 -0
  192. package/vanilla/UiEditable.js +59 -0
  193. package/vanilla/UiEditorInsertTableGrid.d.ts +18 -0
  194. package/vanilla/UiEditorInsertTableGrid.js +46 -0
  195. package/vanilla/UiEditorMentionMenu.d.ts +21 -0
  196. package/vanilla/UiEditorMentionMenu.js +63 -0
  197. package/vanilla/UiEditorSlashMenu.d.ts +20 -0
  198. package/vanilla/UiEditorSlashMenu.js +74 -0
  199. package/vanilla/UiEditorTableContextMenu.d.ts +17 -0
  200. package/vanilla/UiEditorTableContextMenu.js +69 -0
  201. package/vanilla/UiEditorTableOverlay.d.ts +16 -0
  202. package/vanilla/UiEditorTableOverlay.js +113 -0
  203. package/vanilla/UiEditorTableToolbar.d.ts +18 -0
  204. package/vanilla/UiEditorTableToolbar.js +121 -0
  205. package/vanilla/UiEditorToolbar.d.ts +14 -0
  206. package/vanilla/UiEditorToolbar.js +30 -0
  207. package/vanilla/UiFloatingActionButton.d.ts +16 -0
  208. package/vanilla/UiFloatingActionButton.js +35 -0
  209. package/vanilla/UiFormField.d.ts +16 -0
  210. package/vanilla/UiFormField.js +54 -0
  211. package/vanilla/UiGrid.d.ts +15 -0
  212. package/vanilla/UiGrid.js +30 -0
  213. package/vanilla/UiIcon.d.ts +14 -0
  214. package/vanilla/UiIcon.js +40 -0
  215. package/vanilla/UiIconButton.d.ts +19 -0
  216. package/vanilla/UiIconButton.js +37 -0
  217. package/vanilla/UiInput.d.ts +18 -0
  218. package/vanilla/UiInput.js +37 -0
  219. package/vanilla/UiMenu.d.ts +19 -0
  220. package/vanilla/UiMenu.js +35 -0
  221. package/vanilla/UiMenuItem.d.ts +15 -0
  222. package/vanilla/UiMenuItem.js +43 -0
  223. package/vanilla/UiPopover.d.ts +18 -0
  224. package/vanilla/UiPopover.js +33 -0
  225. package/vanilla/UiRadio.d.ts +19 -0
  226. package/vanilla/UiRadio.js +48 -0
  227. package/vanilla/UiRadioGroup.d.ts +21 -0
  228. package/vanilla/UiRadioGroup.js +35 -0
  229. package/vanilla/UiReel.d.ts +16 -0
  230. package/vanilla/UiReel.js +34 -0
  231. package/vanilla/UiSearchResultRow.d.ts +15 -0
  232. package/vanilla/UiSearchResultRow.js +82 -0
  233. package/vanilla/UiSearchShell.d.ts +18 -0
  234. package/vanilla/UiSearchShell.js +74 -0
  235. package/vanilla/UiSelect.d.ts +18 -0
  236. package/vanilla/UiSelect.js +42 -0
  237. package/vanilla/UiSeparator.d.ts +14 -0
  238. package/vanilla/UiSeparator.js +28 -0
  239. package/vanilla/UiSidebar.d.ts +24 -0
  240. package/vanilla/UiSidebar.js +47 -0
  241. package/vanilla/UiStack.d.ts +16 -0
  242. package/vanilla/UiStack.js +32 -0
  243. package/vanilla/UiSwitch.d.ts +18 -0
  244. package/vanilla/UiSwitch.js +46 -0
  245. package/vanilla/UiSwitcher.d.ts +16 -0
  246. package/vanilla/UiSwitcher.js +32 -0
  247. package/vanilla/UiTabs.d.ts +18 -0
  248. package/vanilla/UiTabs.js +47 -0
  249. package/vanilla/UiTextarea.d.ts +19 -0
  250. package/vanilla/UiTextarea.js +40 -0
  251. package/vanilla/UiToastRegion.d.ts +20 -0
  252. package/vanilla/UiToastRegion.js +58 -0
  253. package/vanilla/UiTooltip.d.ts +21 -0
  254. package/vanilla/UiTooltip.js +34 -0
  255. package/vanilla/UiTopBar.d.ts +13 -0
  256. package/vanilla/UiTopBar.js +59 -0
  257. package/vanilla/UiTree.d.ts +18 -0
  258. package/vanilla/UiTree.js +31 -0
  259. package/vanilla/UiTreeItem.d.ts +26 -0
  260. package/vanilla/UiTreeItem.js +164 -0
  261. package/vanilla/index.js +48 -0
  262. package/vue/UiAffordanceScope.d.ts +18 -0
  263. package/vue/UiAffordanceScope.js +2 -0
  264. package/vue/UiAffordanceScope.vue +118 -0
  265. package/vue/UiAvatar.d.ts +23 -0
  266. package/vue/UiAvatar.js +2 -0
  267. package/vue/UiAvatar.vue +152 -0
  268. package/vue/UiAvatarGroup.d.ts +20 -0
  269. package/vue/UiAvatarGroup.js +2 -0
  270. package/vue/UiAvatarGroup.vue +167 -0
  271. package/vue/UiBadge.d.ts +20 -0
  272. package/vue/UiBadge.js +2 -0
  273. package/vue/UiBadge.vue +123 -0
  274. package/vue/UiButton.d.ts +22 -0
  275. package/vue/UiButton.js +2 -0
  276. package/vue/UiButton.vue +257 -0
  277. package/vue/UiCallout.d.ts +18 -0
  278. package/vue/UiCallout.js +2 -0
  279. package/vue/UiCallout.vue +104 -0
  280. package/vue/UiCheckbox.d.ts +38 -0
  281. package/vue/UiCheckbox.js +2 -0
  282. package/vue/UiCheckbox.vue +216 -0
  283. package/vue/UiCluster.d.ts +17 -0
  284. package/vue/UiCluster.js +2 -0
  285. package/vue/UiCluster.vue +75 -0
  286. package/vue/UiCombobox.d.ts +255 -0
  287. package/vue/UiCombobox.js +2 -0
  288. package/vue/UiCombobox.vue +466 -0
  289. package/vue/UiContainer.d.ts +17 -0
  290. package/vue/UiContainer.js +2 -0
  291. package/vue/UiContainer.vue +65 -0
  292. package/vue/UiContextMenu.d.ts +50 -0
  293. package/vue/UiContextMenu.js +2 -0
  294. package/vue/UiContextMenu.vue +166 -0
  295. package/vue/UiDialog.d.ts +39 -0
  296. package/vue/UiDialog.js +2 -0
  297. package/vue/UiDialog.vue +273 -0
  298. package/vue/UiDisclosure.d.ts +44 -0
  299. package/vue/UiDisclosure.js +2 -0
  300. package/vue/UiDisclosure.vue +211 -0
  301. package/vue/UiEditable.d.ts +48 -0
  302. package/vue/UiEditable.js +2 -0
  303. package/vue/UiEditable.vue +267 -0
  304. package/vue/UiEditorInsertTableGrid.d.ts +26 -0
  305. package/vue/UiEditorInsertTableGrid.js +2 -0
  306. package/vue/UiEditorInsertTableGrid.vue +210 -0
  307. package/vue/UiEditorMentionMenu.d.ts +44 -0
  308. package/vue/UiEditorMentionMenu.js +2 -0
  309. package/vue/UiEditorMentionMenu.vue +284 -0
  310. package/vue/UiEditorSlashMenu.d.ts +41 -0
  311. package/vue/UiEditorSlashMenu.js +2 -0
  312. package/vue/UiEditorSlashMenu.vue +306 -0
  313. package/vue/UiEditorTableContextMenu.d.ts +19 -0
  314. package/vue/UiEditorTableContextMenu.js +2 -0
  315. package/vue/UiEditorTableContextMenu.vue +252 -0
  316. package/vue/UiEditorTableOverlay.d.ts +66 -0
  317. package/vue/UiEditorTableOverlay.js +2 -0
  318. package/vue/UiEditorTableOverlay.vue +455 -0
  319. package/vue/UiEditorTableToolbar.d.ts +21 -0
  320. package/vue/UiEditorTableToolbar.js +2 -0
  321. package/vue/UiEditorTableToolbar.vue +339 -0
  322. package/vue/UiEditorToolbar.d.ts +18 -0
  323. package/vue/UiEditorToolbar.js +2 -0
  324. package/vue/UiEditorToolbar.vue +138 -0
  325. package/vue/UiFloatingActionButton.d.ts +22 -0
  326. package/vue/UiFloatingActionButton.js +2 -0
  327. package/vue/UiFloatingActionButton.vue +116 -0
  328. package/vue/UiFormField.d.ts +28 -0
  329. package/vue/UiFormField.js +2 -0
  330. package/vue/UiFormField.vue +124 -0
  331. package/vue/UiGrid.d.ts +17 -0
  332. package/vue/UiGrid.js +2 -0
  333. package/vue/UiGrid.vue +72 -0
  334. package/vue/UiIcon.d.ts +8 -0
  335. package/vue/UiIcon.js +2 -0
  336. package/vue/UiIcon.vue +135 -0
  337. package/vue/UiIconButton.d.ts +27 -0
  338. package/vue/UiIconButton.js +2 -0
  339. package/vue/UiIconButton.vue +281 -0
  340. package/vue/UiInput.d.ts +20 -0
  341. package/vue/UiInput.js +2 -0
  342. package/vue/UiInput.vue +177 -0
  343. package/vue/UiMenu.d.ts +52 -0
  344. package/vue/UiMenu.js +2 -0
  345. package/vue/UiMenu.vue +194 -0
  346. package/vue/UiMenuItem.d.ts +20 -0
  347. package/vue/UiMenuItem.js +2 -0
  348. package/vue/UiMenuItem.vue +55 -0
  349. package/vue/UiPopover.d.ts +44 -0
  350. package/vue/UiPopover.js +2 -0
  351. package/vue/UiPopover.vue +188 -0
  352. package/vue/UiRadio.d.ts +38 -0
  353. package/vue/UiRadio.js +2 -0
  354. package/vue/UiRadio.vue +166 -0
  355. package/vue/UiRadioGroup.d.ts +50 -0
  356. package/vue/UiRadioGroup.js +2 -0
  357. package/vue/UiRadioGroup.vue +161 -0
  358. package/vue/UiReel.d.ts +18 -0
  359. package/vue/UiReel.js +2 -0
  360. package/vue/UiReel.vue +93 -0
  361. package/vue/UiSearchResultRow.d.ts +28 -0
  362. package/vue/UiSearchResultRow.js +2 -0
  363. package/vue/UiSearchResultRow.vue +124 -0
  364. package/vue/UiSearchShell.d.ts +42 -0
  365. package/vue/UiSearchShell.js +2 -0
  366. package/vue/UiSearchShell.vue +233 -0
  367. package/vue/UiSelect.d.ts +30 -0
  368. package/vue/UiSelect.js +2 -0
  369. package/vue/UiSelect.vue +183 -0
  370. package/vue/UiSeparator.d.ts +8 -0
  371. package/vue/UiSeparator.js +2 -0
  372. package/vue/UiSeparator.vue +38 -0
  373. package/vue/UiSidebar.d.ts +54 -0
  374. package/vue/UiSidebar.js +2 -0
  375. package/vue/UiSidebar.vue +288 -0
  376. package/vue/UiStack.d.ts +18 -0
  377. package/vue/UiStack.js +2 -0
  378. package/vue/UiStack.vue +85 -0
  379. package/vue/UiSwitch.d.ts +36 -0
  380. package/vue/UiSwitch.js +2 -0
  381. package/vue/UiSwitch.vue +203 -0
  382. package/vue/UiSwitcher.d.ts +18 -0
  383. package/vue/UiSwitcher.js +2 -0
  384. package/vue/UiSwitcher.vue +91 -0
  385. package/vue/UiTabs.d.ts +36 -0
  386. package/vue/UiTabs.js +2 -0
  387. package/vue/UiTabs.vue +269 -0
  388. package/vue/UiTextarea.d.ts +22 -0
  389. package/vue/UiTextarea.js +2 -0
  390. package/vue/UiTextarea.vue +177 -0
  391. package/vue/UiToastRegion.d.ts +48 -0
  392. package/vue/UiToastRegion.js +2 -0
  393. package/vue/UiToastRegion.vue +256 -0
  394. package/vue/UiTooltip.d.ts +50 -0
  395. package/vue/UiTooltip.js +2 -0
  396. package/vue/UiTooltip.vue +230 -0
  397. package/vue/UiTopBar.d.ts +19 -0
  398. package/vue/UiTopBar.js +2 -0
  399. package/vue/UiTopBar.vue +56 -0
  400. package/vue/UiTree.d.ts +62 -0
  401. package/vue/UiTree.js +2 -0
  402. package/vue/UiTree.vue +117 -0
  403. package/vue/UiTreeItem.d.ts +56 -0
  404. package/vue/UiTreeItem.js +2 -0
  405. package/vue/UiTreeItem.vue +418 -0
  406. package/vue/chunks/UiAffordanceScope.js +112 -0
  407. package/vue/chunks/UiAvatar.js +144 -0
  408. package/vue/chunks/UiAvatarGroup.js +142 -0
  409. package/vue/chunks/UiBadge.js +33 -0
  410. package/vue/chunks/UiButton.js +47 -0
  411. package/vue/chunks/UiCallout.js +117 -0
  412. package/vue/chunks/UiCheckbox.js +167 -0
  413. package/vue/chunks/UiCluster.js +33 -0
  414. package/vue/chunks/UiCombobox.js +684 -0
  415. package/vue/chunks/UiContainer.js +33 -0
  416. package/vue/chunks/UiContextMenu.js +153 -0
  417. package/vue/chunks/UiDialog.js +162 -0
  418. package/vue/chunks/UiDisclosure.js +185 -0
  419. package/vue/chunks/UiEditable.js +223 -0
  420. package/vue/chunks/UiEditorInsertTableGrid.js +221 -0
  421. package/vue/chunks/UiEditorMentionMenu.js +245 -0
  422. package/vue/chunks/UiEditorSlashMenu.js +210 -0
  423. package/vue/chunks/UiEditorTableContextMenu.js +201 -0
  424. package/vue/chunks/UiEditorTableOverlay.js +301 -0
  425. package/vue/chunks/UiEditorTableToolbar.js +299 -0
  426. package/vue/chunks/UiEditorToolbar.js +37 -0
  427. package/vue/chunks/UiFloatingActionButton.js +56 -0
  428. package/vue/chunks/UiFormField.js +113 -0
  429. package/vue/chunks/UiGrid.js +33 -0
  430. package/vue/chunks/UiIcon.js +170 -0
  431. package/vue/chunks/UiIconButton.js +141 -0
  432. package/vue/chunks/UiInput.js +136 -0
  433. package/vue/chunks/UiMenu.js +163 -0
  434. package/vue/chunks/UiMenuItem.js +37 -0
  435. package/vue/chunks/UiPopover.js +149 -0
  436. package/vue/chunks/UiRadio.js +164 -0
  437. package/vue/chunks/UiRadioGroup.js +160 -0
  438. package/vue/chunks/UiReel.js +42 -0
  439. package/vue/chunks/UiSearchResultRow.js +63 -0
  440. package/vue/chunks/UiSearchShell.js +152 -0
  441. package/vue/chunks/UiSelect.js +134 -0
  442. package/vue/chunks/UiSeparator.js +24 -0
  443. package/vue/chunks/UiSidebar.js +180 -0
  444. package/vue/chunks/UiStack.js +38 -0
  445. package/vue/chunks/UiSwitch.js +162 -0
  446. package/vue/chunks/UiSwitcher.js +38 -0
  447. package/vue/chunks/UiTabs.js +210 -0
  448. package/vue/chunks/UiTextarea.js +139 -0
  449. package/vue/chunks/UiToastRegion.js +227 -0
  450. package/vue/chunks/UiTooltip.js +163 -0
  451. package/vue/chunks/UiTopBar.js +25 -0
  452. package/vue/chunks/UiTree.js +140 -0
  453. package/vue/chunks/UiTreeItem.js +315 -0
  454. package/vue/chunks/_plugin-vue_export-helper.js +8 -0
  455. package/vue/components.css +4442 -0
  456. package/vue/editor/LoomaEditor.d.ts +171 -0
  457. package/vue/editor/image-delivery.d.ts +61 -0
  458. package/vue/editor/index.d.ts +4 -386
  459. package/vue/editor/index.js +995 -1134
  460. package/vue/editor/primitives.d.ts +23 -0
  461. package/vue/index.d.ts +49 -604
  462. package/vue/index.js +50 -86
  463. package/dist/chunk-MLKGABMK.js +0 -9
  464. package/dist/chunk-RWLYVQY6.js +0 -5719
  465. package/dist/chunk-XDNZIKNI.js +0 -2721
  466. package/dist/declarative-generated.cjs +0 -149
  467. package/dist/declarative-generated.d.cts +0 -16
  468. package/dist/declarative-generated.d.ts +0 -16
  469. package/dist/declarative-generated.js +0 -124
  470. package/dist/declarative.cjs +0 -2750
  471. package/dist/declarative.d.cts +0 -101
  472. package/dist/declarative.d.ts +0 -101
  473. package/dist/declarative.js +0 -15
  474. package/dist/index.cjs +0 -9077
  475. package/dist/index.d.cts +0 -414
  476. package/dist/loader.cjs +0 -8442
  477. package/dist/loader.d.cts +0 -4
  478. package/dist/loader.d.ts +0 -4
  479. package/dist/loader.js +0 -16
  480. package/editor/chunk-EUIH3TBV.js +0 -6826
  481. package/editor/chunk-JK53ENSG.js +0 -29
  482. package/editor/chunk-U36XNFJN.js +0 -2592
  483. package/editor/table-overlay-DZFGHQDt.d.ts +0 -138
  484. package/editor.css +0 -1592
  485. package/layout/index.cjs +0 -356
  486. package/layout/index.d.cts +0 -18
  487. package/layout/index.d.ts +0 -18
  488. package/layout/index.js +0 -341
  489. package/layout.css +0 -191
  490. package/styles.css +0 -940
  491. package/vue/chunk-OX3EACZ7.js +0 -4369
@@ -1,1171 +1,1032 @@
1
- import {
2
- IconButton,
3
- Popover,
4
- UiEditorInsertTableGrid_default,
5
- UiEditorMentionMenu_default,
6
- UiEditorSlashMenu_default,
7
- UiEditorTableContextMenu_default,
8
- UiEditorTableOverlay_default,
9
- UiEditorTableToolbar_default,
10
- UiEditorToolbar_default,
11
- createAdapterComponent,
12
- toHTMLElement
13
- } from "../chunk-OX3EACZ7.js";
14
-
15
- // src/editor/index.ts
16
- import "@threadlabs/looma/editor";
17
- export * from "@threadlabs/looma/editor";
18
-
19
- // src/editor/primitives.ts
20
- import {
21
- defineComponent,
22
- h,
23
- shallowRef,
24
- watchEffect
25
- } from "vue";
26
- function createSuggestionMenuAdapter(name) {
27
- const component = name === "EditorSlashMenu" ? UiEditorSlashMenu_default : UiEditorMentionMenu_default;
28
- return defineComponent({
29
- name,
30
- inheritAttrs: false,
31
- props: {
32
- open: { type: Boolean, default: false },
33
- query: { type: String, default: "" },
34
- items: { type: Array, default: () => [] },
35
- selectedIndex: { type: Number, default: 0 },
36
- anchorRect: { type: Object, default: null },
37
- loading: { type: Boolean, default: false }
38
- },
39
- setup(props, { attrs }) {
40
- const elementRef = shallowRef(null);
41
- watchEffect((onCleanup) => {
42
- const element = elementRef.value;
43
- if (!element) return;
44
- const callbackAttrs = attrs;
45
- const highlightCallback = callbackAttrs.onHighlight;
46
- const selectCallback = callbackAttrs.onSelect;
47
- const highlightHandler = typeof highlightCallback === "function" ? (event) => highlightCallback(event.detail) : void 0;
48
- const selectHandler = typeof selectCallback === "function" ? (event) => selectCallback(event.detail) : void 0;
49
- if (highlightHandler) element.addEventListener("highlight", highlightHandler);
50
- if (selectHandler) element.addEventListener("select", selectHandler);
51
- onCleanup(() => {
52
- if (highlightHandler) element.removeEventListener("highlight", highlightHandler);
53
- if (selectHandler) element.removeEventListener("select", selectHandler);
54
- });
55
- });
56
- return () => {
57
- const callbackAttrs = attrs;
58
- const {
59
- onHighlight,
60
- onSelect,
61
- ...forwardedAttrs
62
- } = callbackAttrs;
63
- void onHighlight;
64
- void onSelect;
65
- return h(component, {
66
- ...props,
67
- ...forwardedAttrs,
68
- ref: (value) => {
69
- elementRef.value = toHTMLElement(value);
70
- }
71
- });
72
- };
73
- }
74
- });
75
- }
76
- var ACTION_EVENT_BINDINGS = [["action", "onAction"]];
77
- var INSERT_EVENT_BINDINGS = [["insert", "onInsert"]];
78
- var EditorSlashMenu = createSuggestionMenuAdapter(
79
- "EditorSlashMenu"
80
- );
81
- var EditorMentionMenu = createSuggestionMenuAdapter(
82
- "EditorMentionMenu"
83
- );
84
- var EditorToolbar = createAdapterComponent(UiEditorToolbar_default, "EditorToolbar");
85
- var EditorTableContextMenu = createAdapterComponent(UiEditorTableContextMenu_default, "EditorTableContextMenu", ACTION_EVENT_BINDINGS);
86
- var EditorTableToolbar = createAdapterComponent(UiEditorTableToolbar_default, "EditorTableToolbar", ACTION_EVENT_BINDINGS);
87
- var EditorInsertTableGrid = createAdapterComponent(UiEditorInsertTableGrid_default, "EditorInsertTableGrid", INSERT_EVENT_BINDINGS);
88
- var EditorTableOverlay = createAdapterComponent(
89
- UiEditorTableOverlay_default,
90
- "EditorTableOverlay",
91
- ACTION_EVENT_BINDINGS,
92
- "class",
93
- ["geometry"]
94
- );
95
- var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
96
- EditorToolbar: "ui-editor-toolbar",
97
- EditorSlashMenu: "ui-editor-slash-menu",
98
- EditorMentionMenu: "ui-editor-mention-menu",
99
- EditorTableContextMenu: "ui-editor-table-context-menu",
100
- EditorTableToolbar: "ui-editor-table-toolbar",
101
- EditorInsertTableGrid: "ui-editor-insert-table-grid",
102
- EditorTableOverlay: "ui-editor-table-overlay"
103
- };
104
-
105
- // src/editor/LoomaEditor.ts
1
+ import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
2
+ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
3
+ import { EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Popover } from "@threadlabs/looma/vue";
106
4
  import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
107
- import { TextSelection } from "@tiptap/pm/state";
108
- import {
109
- defineComponent as defineComponent2,
110
- h as h2,
111
- nextTick,
112
- onBeforeUnmount,
113
- onMounted,
114
- reactive,
115
- ref,
116
- useId,
117
- watch
118
- } from "vue";
119
- import {
120
- createLoomaSlashCommandExtension,
121
- createLoomaMentionExtension,
122
- DEFAULT_MENTION_RESULT_LIMIT,
123
- getActiveTableUiState,
124
- getDefaultEditorExtensions,
125
- handleTableAction,
126
- handleTableOverlayAction,
127
- measureTableOverlayGeometry,
128
- normalizeActiveTableColumnWidths,
129
- resolveTableCellAt,
130
- shouldShowTextFormattingToolbar
131
- } from "@threadlabs/looma/editor";
132
- import { getVisualViewportRect, LOOMA_ICONS } from "@threadlabs/looma/core";
133
-
134
- // src/editor/image-delivery.ts
5
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
135
6
  import { Extension } from "@tiptap/core";
136
- import { Plugin, PluginKey } from "@tiptap/pm/state";
137
7
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
+ export * from "@threadlabs/looma/editor";
9
+ //#region src/vue/editor/primitives.ts
10
+ /** Each Vue editor component's element tag, for tooling that enumerates them. */
11
+ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
12
+ EditorToolbar: "ui-editor-toolbar",
13
+ EditorSlashMenu: "ui-editor-slash-menu",
14
+ EditorMentionMenu: "ui-editor-mention-menu",
15
+ EditorTableContextMenu: "ui-editor-table-context-menu",
16
+ EditorTableToolbar: "ui-editor-table-toolbar",
17
+ EditorInsertTableGrid: "ui-editor-insert-table-grid",
18
+ EditorTableOverlay: "ui-editor-table-overlay"
19
+ };
20
+ //#endregion
21
+ //#region src/vue/editor/image-delivery.ts
138
22
  function optionalString(value) {
139
- return typeof value === "string" && value.length > 0 ? value : void 0;
23
+ return typeof value === "string" && value.length > 0 ? value : void 0;
140
24
  }
141
25
  function normalizeImageDimension(value) {
142
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
26
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
143
27
  }
144
28
  function imageDescriptorFromAttrs(attrs) {
145
- const src = optionalString(attrs.src);
146
- if (!src) return null;
147
- const alt = typeof attrs.alt === "string" ? attrs.alt : void 0;
148
- const width = normalizeImageDimension(attrs.width);
149
- const height = normalizeImageDimension(attrs.height);
150
- return {
151
- src,
152
- ...alt !== void 0 ? { alt } : {},
153
- ...width ? { width } : {},
154
- ...height ? { height } : {},
155
- ...attrs.responsive === true ? { responsive: true } : {}
156
- };
29
+ const src = optionalString(attrs.src);
30
+ if (!src) return null;
31
+ const alt = typeof attrs.alt === "string" ? attrs.alt : void 0;
32
+ const width = normalizeImageDimension(attrs.width);
33
+ const height = normalizeImageDimension(attrs.height);
34
+ return {
35
+ src,
36
+ ...alt !== void 0 ? { alt } : {},
37
+ ...width ? { width } : {},
38
+ ...height ? { height } : {},
39
+ ...attrs.responsive === true ? { responsive: true } : {}
40
+ };
157
41
  }
158
42
  function transientAttributes(resolver, image) {
159
- const attributes = {
160
- "aria-label": image.alt ? `Open image: ${image.alt}` : "Open image",
161
- "data-looma-image": "",
162
- role: "button",
163
- src: image.src,
164
- tabindex: "0"
165
- };
166
- if (!image.responsive || !resolver) return attributes;
167
- let rendition;
168
- try {
169
- rendition = resolver(image);
170
- } catch {
171
- return attributes;
172
- }
173
- if (!rendition) return attributes;
174
- const src = optionalString(rendition.src);
175
- const srcset = optionalString(rendition.srcset);
176
- const sizes = optionalString(rendition.sizes);
177
- if (src) attributes.src = src;
178
- if (srcset) attributes.srcset = srcset;
179
- if (sizes) attributes.sizes = sizes;
180
- if (rendition.loading === "eager" || rendition.loading === "lazy") {
181
- attributes.loading = rendition.loading;
182
- }
183
- if (["async", "auto", "sync"].includes(rendition.decoding ?? "")) {
184
- attributes.decoding = rendition.decoding;
185
- }
186
- if (["auto", "high", "low"].includes(rendition.fetchPriority ?? "")) {
187
- attributes.fetchpriority = rendition.fetchPriority;
188
- }
189
- if (src && src !== image.src || srcset) attributes["data-looma-rendition"] = "true";
190
- return attributes;
43
+ const attributes = {
44
+ "aria-label": image.alt ? `Open image: ${image.alt}` : "Open image",
45
+ "data-looma-image": "",
46
+ role: "button",
47
+ src: image.src,
48
+ tabindex: "0"
49
+ };
50
+ if (!image.responsive || !resolver) return attributes;
51
+ let rendition;
52
+ try {
53
+ rendition = resolver(image);
54
+ } catch {
55
+ return attributes;
56
+ }
57
+ if (!rendition) return attributes;
58
+ const src = optionalString(rendition.src);
59
+ const srcset = optionalString(rendition.srcset);
60
+ const sizes = optionalString(rendition.sizes);
61
+ if (src) attributes.src = src;
62
+ if (srcset) attributes.srcset = srcset;
63
+ if (sizes) attributes.sizes = sizes;
64
+ if (rendition.loading === "eager" || rendition.loading === "lazy") attributes.loading = rendition.loading;
65
+ if ([
66
+ "async",
67
+ "auto",
68
+ "sync"
69
+ ].includes(rendition.decoding ?? "")) attributes.decoding = rendition.decoding;
70
+ if ([
71
+ "auto",
72
+ "high",
73
+ "low"
74
+ ].includes(rendition.fetchPriority ?? "")) attributes.fetchpriority = rendition.fetchPriority;
75
+ if (src && src !== image.src || srcset) attributes["data-looma-rendition"] = "true";
76
+ return attributes;
191
77
  }
192
78
  function createDecorations(doc, resolver, failedRenditions) {
193
- const decorations = [];
194
- const currentSources = /* @__PURE__ */ new Set();
195
- doc.descendants((node, position) => {
196
- if (node.type.name !== "image") return;
197
- const image = imageDescriptorFromAttrs(node.attrs);
198
- if (!image) return;
199
- currentSources.add(image.src);
200
- decorations.push(Decoration.node(
201
- position,
202
- position + node.nodeSize,
203
- transientAttributes(failedRenditions.has(image.src) ? void 0 : resolver, image)
204
- ));
205
- });
206
- for (const src of failedRenditions) {
207
- if (!currentSources.has(src)) failedRenditions.delete(src);
208
- }
209
- return DecorationSet.create(doc, decorations);
79
+ const decorations = [];
80
+ const currentSources = /* @__PURE__ */ new Set();
81
+ doc.descendants((node, position) => {
82
+ if (node.type.name !== "image") return;
83
+ const image = imageDescriptorFromAttrs(node.attrs);
84
+ if (!image) return;
85
+ currentSources.add(image.src);
86
+ decorations.push(Decoration.node(position, position + node.nodeSize, transientAttributes(failedRenditions.has(image.src) ? void 0 : resolver, image)));
87
+ });
88
+ for (const src of failedRenditions) if (!currentSources.has(src)) failedRenditions.delete(src);
89
+ return DecorationSet.create(doc, decorations);
210
90
  }
211
91
  function rangeContainsImage(doc, from, to) {
212
- const start = Math.max(0, Math.min(from - 1, doc.content.size));
213
- const end = Math.max(start, Math.min(to + 1, doc.content.size));
214
- let found = false;
215
- doc.nodesBetween(start, end, (node) => {
216
- if (node.type.name !== "image") return !found;
217
- found = true;
218
- return false;
219
- });
220
- return found;
92
+ const start = Math.max(0, Math.min(from - 1, doc.content.size));
93
+ const end = Math.max(start, Math.min(to + 1, doc.content.size));
94
+ let found = false;
95
+ doc.nodesBetween(start, end, (node) => {
96
+ if (node.type.name !== "image") return !found;
97
+ found = true;
98
+ return false;
99
+ });
100
+ return found;
221
101
  }
222
102
  function transactionChangesImages(transaction) {
223
- let changed = false;
224
- transaction.mapping.maps.forEach((stepMap) => {
225
- stepMap.forEach((oldStart, oldEnd, newStart, newEnd) => {
226
- if (rangeContainsImage(transaction.before, oldStart, oldEnd) || rangeContainsImage(transaction.doc, newStart, newEnd)) changed = true;
227
- });
228
- });
229
- return changed;
103
+ let changed = false;
104
+ transaction.mapping.maps.forEach((stepMap) => {
105
+ stepMap.forEach((oldStart, oldEnd, newStart, newEnd) => {
106
+ if (rangeContainsImage(transaction.before, oldStart, oldEnd) || rangeContainsImage(transaction.doc, newStart, newEnd)) changed = true;
107
+ });
108
+ });
109
+ return changed;
230
110
  }
231
111
  function createLoomaImageDeliveryController(options) {
232
- const pluginKey = new PluginKey("looma-image-delivery");
233
- const failedRenditions = /* @__PURE__ */ new Set();
234
- const refresh = (editor) => {
235
- editor.view.dispatch(editor.state.tr.setMeta(pluginKey, true));
236
- };
237
- const extension = Extension.create({
238
- name: "loomaImageDelivery",
239
- addGlobalAttributes() {
240
- return [{
241
- types: ["image"],
242
- attributes: {
243
- width: {
244
- default: null,
245
- parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("width"))) ?? null,
246
- renderHTML: (attributes) => normalizeImageDimension(attributes.width) ? { width: normalizeImageDimension(attributes.width) } : {}
247
- },
248
- height: {
249
- default: null,
250
- parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("height"))) ?? null,
251
- renderHTML: (attributes) => normalizeImageDimension(attributes.height) ? { height: normalizeImageDimension(attributes.height) } : {}
252
- },
253
- responsive: {
254
- default: null,
255
- parseHTML: (element) => element.hasAttribute("data-looma-responsive") || null,
256
- renderHTML: (attributes) => attributes.responsive === true ? { "data-looma-responsive": "" } : {}
257
- }
258
- }
259
- }];
260
- },
261
- addProseMirrorPlugins() {
262
- return [new Plugin({
263
- key: pluginKey,
264
- state: {
265
- init: (_, state) => createDecorations(
266
- state.doc,
267
- options.resolveAttributes(),
268
- failedRenditions
269
- ),
270
- apply: (transaction, previous) => {
271
- if (!transaction.getMeta(pluginKey) && !transactionChangesImages(transaction)) {
272
- return previous.map(transaction.mapping, transaction.doc);
273
- }
274
- return createDecorations(
275
- transaction.doc,
276
- options.resolveAttributes(),
277
- failedRenditions
278
- );
279
- }
280
- },
281
- props: {
282
- decorations: (state) => pluginKey.getState(state)
283
- }
284
- })];
285
- }
286
- });
287
- return {
288
- extension,
289
- markRenditionFailed: (editor, src) => {
290
- if (failedRenditions.has(src)) return false;
291
- failedRenditions.add(src);
292
- refresh(editor);
293
- return true;
294
- },
295
- reset: (editor) => {
296
- failedRenditions.clear();
297
- refresh(editor);
298
- }
299
- };
112
+ const pluginKey = new PluginKey("looma-image-delivery");
113
+ const failedRenditions = /* @__PURE__ */ new Set();
114
+ const refresh = (editor) => {
115
+ editor.view.dispatch(editor.state.tr.setMeta(pluginKey, true));
116
+ };
117
+ return {
118
+ extension: Extension.create({
119
+ name: "loomaImageDelivery",
120
+ addGlobalAttributes() {
121
+ return [{
122
+ types: ["image"],
123
+ attributes: {
124
+ width: {
125
+ default: null,
126
+ parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("width"))) ?? null,
127
+ renderHTML: (attributes) => normalizeImageDimension(attributes.width) ? { width: normalizeImageDimension(attributes.width) } : {}
128
+ },
129
+ height: {
130
+ default: null,
131
+ parseHTML: (element) => normalizeImageDimension(Number(element.getAttribute("height"))) ?? null,
132
+ renderHTML: (attributes) => normalizeImageDimension(attributes.height) ? { height: normalizeImageDimension(attributes.height) } : {}
133
+ },
134
+ responsive: {
135
+ default: null,
136
+ parseHTML: (element) => element.hasAttribute("data-looma-responsive") || null,
137
+ renderHTML: (attributes) => attributes.responsive === true ? { "data-looma-responsive": "" } : {}
138
+ }
139
+ }
140
+ }];
141
+ },
142
+ addProseMirrorPlugins() {
143
+ return [new Plugin({
144
+ key: pluginKey,
145
+ state: {
146
+ init: (_, state) => createDecorations(state.doc, options.resolveAttributes(), failedRenditions),
147
+ apply: (transaction, previous) => {
148
+ if (!transaction.getMeta(pluginKey) && !transactionChangesImages(transaction)) return previous.map(transaction.mapping, transaction.doc);
149
+ return createDecorations(transaction.doc, options.resolveAttributes(), failedRenditions);
150
+ }
151
+ },
152
+ props: { decorations: (state) => pluginKey.getState(state) }
153
+ })];
154
+ }
155
+ }),
156
+ markRenditionFailed: (editor, src) => {
157
+ if (failedRenditions.has(src)) return false;
158
+ failedRenditions.add(src);
159
+ refresh(editor);
160
+ return true;
161
+ },
162
+ reset: (editor) => {
163
+ failedRenditions.clear();
164
+ refresh(editor);
165
+ }
166
+ };
300
167
  }
301
168
  function imageUploadContent(image) {
302
- return {
303
- type: "image",
304
- attrs: {
305
- src: image.src,
306
- alt: image.alt,
307
- ...image.width ? { width: image.width } : {},
308
- ...image.height ? { height: image.height } : {},
309
- ...image.responsive === true ? { responsive: true } : {}
310
- }
311
- };
169
+ return {
170
+ type: "image",
171
+ attrs: {
172
+ src: image.src,
173
+ alt: image.alt,
174
+ ...image.width ? { width: image.width } : {},
175
+ ...image.height ? { height: image.height } : {},
176
+ ...image.responsive === true ? { responsive: true } : {}
177
+ }
178
+ };
312
179
  }
313
-
314
- // src/editor/LoomaEditor.ts
315
- var EMPTY_DOCUMENT = { type: "doc", content: [] };
180
+ //#endregion
181
+ //#region src/vue/editor/LoomaEditor.ts
182
+ var EMPTY_DOCUMENT = {
183
+ type: "doc",
184
+ content: []
185
+ };
316
186
  var editorInstanceSequence = 0;
317
187
  var EMPTY_CAPABILITIES = {
318
- canAddRowBefore: false,
319
- canAddRowAfter: false,
320
- canAddColumnBefore: false,
321
- canAddColumnAfter: false,
322
- canDeleteRow: false,
323
- canDeleteColumn: false,
324
- canDeleteTable: false,
325
- canMergeCells: false,
326
- canSplitCell: false
188
+ canAddRowBefore: false,
189
+ canAddRowAfter: false,
190
+ canAddColumnBefore: false,
191
+ canAddColumnAfter: false,
192
+ canDeleteRow: false,
193
+ canDeleteColumn: false,
194
+ canDeleteTable: false,
195
+ canMergeCells: false,
196
+ canSplitCell: false
327
197
  };
328
198
  function sameDocument(left, right) {
329
- return JSON.stringify(left) === JSON.stringify(right);
199
+ return JSON.stringify(left) === JSON.stringify(right);
330
200
  }
331
201
  function managedMenuAnchorRect(rect) {
332
- if (!rect) return null;
333
- return {
334
- left: rect.left,
335
- top: rect.top,
336
- right: rect.right,
337
- bottom: rect.bottom,
338
- x: rect.x,
339
- y: rect.y,
340
- width: rect.width,
341
- height: rect.height
342
- };
202
+ if (!rect) return null;
203
+ return {
204
+ left: rect.left,
205
+ top: rect.top,
206
+ right: rect.right,
207
+ bottom: rect.bottom,
208
+ x: rect.x,
209
+ y: rect.y,
210
+ width: rect.width,
211
+ height: rect.height
212
+ };
343
213
  }
344
214
  function managedSlashMenuItems(items) {
345
- return items.map(({ title, description, icon }) => ({
346
- title,
347
- description,
348
- icon
349
- }));
215
+ return items.map(({ title, description, icon }) => ({
216
+ title,
217
+ description,
218
+ icon
219
+ }));
350
220
  }
351
221
  function selectedTableElement(editor) {
352
- const { node } = editor.view.domAtPos(editor.state.selection.from);
353
- const element = node instanceof HTMLElement ? node : node.parentElement;
354
- return element?.closest("table");
222
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
223
+ return (node instanceof HTMLElement ? node : node.parentElement)?.closest("table");
355
224
  }
356
225
  function selectedTableCellElement(editor) {
357
- const { node } = editor.view.domAtPos(editor.state.selection.from);
358
- const element = node instanceof HTMLElement ? node : node.parentElement;
359
- return element?.closest("td, th");
226
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
227
+ return (node instanceof HTMLElement ? node : node.parentElement)?.closest("td, th");
360
228
  }
361
229
  function loomaIcon(name) {
362
- return h2("svg", {
363
- class: "looma-icon",
364
- "data-looma-icon": name,
365
- "aria-hidden": "true",
366
- focusable: "false",
367
- viewBox: "0 0 24 24",
368
- fill: "none",
369
- stroke: "currentColor",
370
- "stroke-width": 2,
371
- "stroke-linecap": "round",
372
- "stroke-linejoin": "round"
373
- }, LOOMA_ICONS[name].map(([tag, attributes]) => h2(tag, attributes)));
230
+ return h("svg", {
231
+ class: "looma-icon",
232
+ "data-looma-icon": name,
233
+ "aria-hidden": "true",
234
+ focusable: "false",
235
+ viewBox: "0 0 24 24",
236
+ fill: "none",
237
+ stroke: "currentColor",
238
+ "stroke-width": 2,
239
+ "stroke-linecap": "round",
240
+ "stroke-linejoin": "round"
241
+ }, LOOMA_ICONS[name].map(([tag, attributes]) => h(tag, attributes)));
374
242
  }
375
- var LoomaEditor = defineComponent2({
376
- name: "LoomaEditor",
377
- inheritAttrs: false,
378
- props: {
379
- modelValue: {
380
- type: Object,
381
- default: () => ({ ...EMPTY_DOCUMENT })
382
- },
383
- // Deliberate exception to Looma's opt-in boolean rule: an editor that is
384
- // read-only unless configured would violate the primary UX promised by this wrapper.
385
- editable: { type: Boolean, default: true },
386
- placeholder: {
387
- type: String,
388
- default: "Type \u201C/\u201D for commands, or start writing\u2026"
389
- },
390
- extensions: {
391
- type: Array,
392
- default: () => []
393
- },
394
- mentionItems: {
395
- type: Array,
396
- default: () => []
397
- },
398
- mentionProvider: {
399
- type: Function,
400
- default: void 0
401
- },
402
- mentionLimit: {
403
- type: Number,
404
- default: DEFAULT_MENTION_RESULT_LIMIT
405
- },
406
- uploadImage: {
407
- type: Function,
408
- default: void 0
409
- },
410
- resolveImageAttributes: {
411
- type: Function,
412
- default: void 0
413
- },
414
- toolbarMode: {
415
- type: String,
416
- default: "bubble"
417
- }
418
- },
419
- emits: {
420
- "update:modelValue": (_value) => true,
421
- update: (_value) => true,
422
- ready: (_editor) => true,
423
- uploadError: (_error, _file) => true,
424
- imageActivate: (_detail) => true,
425
- imageRenditionError: (_detail) => true
426
- },
427
- setup(props, { attrs, emit, expose }) {
428
- const root = ref(null);
429
- const fileInput = ref(null);
430
- const tablePickerAnchorId = `looma-editor-table-picker-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
431
- const tableToolbarShell = ref(null);
432
- const tableOverlayShell = ref(null);
433
- const tableMenuShell = ref(null);
434
- const mobileToolbarShell = ref(null);
435
- const dragOver = ref(false);
436
- const uploading = ref(false);
437
- const failedUpload = ref(null);
438
- const tablePickerOpen = ref(false);
439
- const mobileToolbarStyle = ref({});
440
- const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
441
- const editorFocused = ref(false);
442
- const editorStateVersion = ref(0);
443
- const mobileToolbarMode = ref("formatting");
444
- let dragLeaveTimer = null;
445
- let tableResizeActive = false;
446
- let tableInteractionActive = false;
447
- let mobileTableControlsDismissed = false;
448
- let hoveredTableCell = null;
449
- let overlayTableElement = null;
450
- let tablePointerFrame = null;
451
- const slash = reactive({
452
- active: false,
453
- items: [],
454
- selectedIndex: 0,
455
- query: "",
456
- rect: null,
457
- select: null
458
- });
459
- const mention = reactive({
460
- active: false,
461
- items: [],
462
- selectedIndex: 0,
463
- query: "",
464
- rect: null,
465
- loading: false,
466
- highlight: null,
467
- select: null
468
- });
469
- const tableUi = reactive({
470
- toolbarOpen: false,
471
- overlayOpen: false,
472
- menuOpen: false,
473
- alignment: "left",
474
- background: null,
475
- rows: 0,
476
- cols: 0,
477
- geometry: null,
478
- toolbarStyle: {},
479
- overlayStyle: {},
480
- menuStyle: {},
481
- capabilities: { ...EMPTY_CAPABILITIES }
482
- });
483
- const slashExtension = createLoomaSlashCommandExtension({
484
- onOpenImagePicker: () => fileInput.value?.click(),
485
- onStateChange: (state) => {
486
- Object.assign(slash, state);
487
- }
488
- });
489
- const mentionMenuId = `looma-editor-mention-menu-${++editorInstanceSequence}`;
490
- const hasConsumerMention = props.extensions.some((extension) => extension.name === "mention");
491
- const mentionExtension = hasConsumerMention ? null : createLoomaMentionExtension({
492
- limit: props.mentionLimit,
493
- menuId: mentionMenuId,
494
- items: async (query, context) => {
495
- if (props.mentionProvider) {
496
- return await props.mentionProvider(query, context);
497
- }
498
- return props.mentionItems;
499
- },
500
- onStateChange: (state) => {
501
- Object.assign(mention, state);
502
- }
503
- });
504
- const imageDelivery = createLoomaImageDeliveryController({
505
- resolveAttributes: () => props.resolveImageAttributes
506
- });
507
- let lastFocusedSelection = null;
508
- const rememberSelection = (instance) => {
509
- if (!instance.isFocused) return;
510
- const selection = instance.view.state.selection;
511
- lastFocusedSelection = {
512
- bookmark: selection.getBookmark(),
513
- from: selection.from,
514
- to: selection.to,
515
- text: selection instanceof TextSelection
516
- };
517
- };
518
- const editor = useEditor({
519
- extensions: [
520
- ...getDefaultEditorExtensions({
521
- placeholder: props.placeholder,
522
- mention: mentionExtension ?? false
523
- }),
524
- imageDelivery.extension,
525
- slashExtension,
526
- ...props.extensions
527
- ],
528
- content: props.modelValue,
529
- editable: props.editable,
530
- onCreate: ({ editor: instance }) => emit("ready", instance),
531
- onFocus: ({ editor: instance }) => rememberSelection(instance),
532
- onSelectionUpdate: ({ editor: instance }) => rememberSelection(instance),
533
- onTransaction: () => {
534
- editorStateVersion.value += 1;
535
- },
536
- onUpdate: ({ editor: instance }) => {
537
- const value = instance.getJSON();
538
- emit("update:modelValue", value);
539
- emit("update", value);
540
- }
541
- });
542
- watch(() => props.editable, (editable) => editor.value?.setEditable(editable));
543
- watch(() => props.resolveImageAttributes, () => {
544
- const instance = editor.value;
545
- if (instance) imageDelivery.reset(instance);
546
- });
547
- watch(() => props.modelValue, (value) => {
548
- const instance = editor.value;
549
- if (!instance || sameDocument(instance.getJSON(), value)) return;
550
- const wasFocused = instance.isFocused;
551
- const previousSelection = lastFocusedSelection ?? {
552
- bookmark: instance.view.state.selection.getBookmark(),
553
- from: instance.view.state.selection.from,
554
- to: instance.view.state.selection.to,
555
- text: instance.view.state.selection instanceof TextSelection
556
- };
557
- if (!wasFocused) {
558
- instance.commands.setContent(value, false);
559
- return;
560
- }
561
- instance.chain().setContent(value, false).command(({ tr }) => {
562
- try {
563
- tr.setSelection(previousSelection.text ? TextSelection.create(tr.doc, previousSelection.from, previousSelection.to) : previousSelection.bookmark.resolve(tr.doc));
564
- } catch {
565
- const max = tr.doc.content.size;
566
- const from = Math.max(1, Math.min(previousSelection.from, max));
567
- const to = Math.max(from, Math.min(previousSelection.to, max));
568
- tr.setSelection(TextSelection.create(tr.doc, from, to));
569
- }
570
- return true;
571
- }).run();
572
- instance.view.focus();
573
- rememberSelection(instance);
574
- }, { deep: true });
575
- const closeTableUi = () => {
576
- tableUi.toolbarOpen = false;
577
- tableUi.overlayOpen = false;
578
- tableUi.menuOpen = false;
579
- tableUi.alignment = "left";
580
- tableUi.background = null;
581
- tableUi.geometry = null;
582
- tableUi.capabilities = { ...EMPTY_CAPABILITIES };
583
- overlayTableElement = null;
584
- mobileTableControlsDismissed = false;
585
- mobileToolbarMode.value = "formatting";
586
- };
587
- const updateMobileViewport = () => {
588
- mobile.value = window.innerWidth <= 767;
589
- if (!mobile.value) {
590
- mobileToolbarStyle.value = {};
591
- return;
592
- }
593
- nextTick(() => {
594
- const viewport = getVisualViewportRect(window);
595
- const toolbarHeight = mobileToolbarShell.value?.getBoundingClientRect().height || 56;
596
- mobileToolbarStyle.value = {
597
- left: `${viewport.left}px`,
598
- top: `${Math.max(viewport.top, viewport.bottom - toolbarHeight)}px`,
599
- width: `${viewport.width}px`
600
- };
601
- });
602
- };
603
- const updateTableUi = () => {
604
- const instance = editor.value;
605
- if (!instance || !props.editable || !instance.isFocused && !tableInteractionActive && !hoveredTableCell) {
606
- closeTableUi();
607
- return;
608
- }
609
- const state = getActiveTableUiState(instance);
610
- const selectedTable = selectedTableElement(instance);
611
- const hoveredTable = hoveredTableCell?.closest("table") ?? null;
612
- const table = hoveredTable ?? selectedTable;
613
- if (!table) {
614
- closeTableUi();
615
- return;
616
- }
617
- const selectedTableIsActive = state.active && selectedTable === table;
618
- overlayTableElement = table;
619
- if (selectedTableIsActive && !mobileTableControlsDismissed) mobileToolbarMode.value = "table";
620
- const rect = table.getBoundingClientRect();
621
- const maxToolbarWidth = Math.min(420, window.innerWidth - 24);
622
- const toolbarLeft = Math.min(
623
- Math.max(12, rect.left + (rect.width - maxToolbarWidth) / 2),
624
- Math.max(12, window.innerWidth - maxToolbarWidth - 12)
625
- );
626
- tableUi.toolbarOpen = selectedTableIsActive && state.showToolbar && !mobile.value;
627
- tableUi.overlayOpen = !mobile.value;
628
- tableUi.alignment = selectedTableIsActive ? state.cellAlignment : "left";
629
- tableUi.background = selectedTableIsActive ? state.cellBackground : null;
630
- tableUi.capabilities = selectedTableIsActive ? state.capabilities : { ...EMPTY_CAPABILITIES };
631
- const selectedCell = selectedTableIsActive ? selectedTableCellElement(instance) : null;
632
- const geometry = measureTableOverlayGeometry(
633
- table,
634
- selectedCell,
635
- hoveredTableCell && table.contains(hoveredTableCell) ? hoveredTableCell : null
636
- );
637
- tableUi.rows = Math.max(0, geometry.rowBoundaries.length - 1);
638
- tableUi.cols = Math.max(0, geometry.columnBoundaries.length - 1);
639
- tableUi.geometry = geometry;
640
- tableUi.toolbarStyle = {
641
- top: `${rect.top > 76 ? rect.top - 52 : rect.bottom + 12}px`,
642
- left: `${toolbarLeft}px`,
643
- maxWidth: `${maxToolbarWidth}px`
644
- };
645
- tableUi.overlayStyle = {
646
- top: `${rect.top}px`,
647
- left: `${rect.left}px`,
648
- width: `${rect.width}px`,
649
- height: `${rect.height}px`
650
- };
651
- };
652
- const onEditorFocus = () => {
653
- editorFocused.value = true;
654
- updateTableUi();
655
- updateMobileViewport();
656
- };
657
- const onEditorBlur = () => {
658
- updateTableUi();
659
- setTimeout(() => {
660
- if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
661
- }, 0);
662
- };
663
- const bindEditorUi = (instance) => {
664
- if (!instance) return;
665
- instance.on("selectionUpdate", updateTableUi);
666
- instance.on("transaction", updateTableUi);
667
- instance.on("focus", onEditorFocus);
668
- instance.on("blur", onEditorBlur);
669
- nextTick(updateTableUi);
670
- };
671
- const unbindEditorUi = (instance) => {
672
- if (!instance) return;
673
- instance.off("selectionUpdate", updateTableUi);
674
- instance.off("transaction", updateTableUi);
675
- instance.off("focus", onEditorFocus);
676
- instance.off("blur", onEditorBlur);
677
- };
678
- watch(editor, (instance, previous) => {
679
- unbindEditorUi(previous);
680
- bindEditorUi(instance);
681
- }, { immediate: true });
682
- const onViewportChange = () => {
683
- updateMobileViewport();
684
- updateTableUi();
685
- tableUi.menuOpen = false;
686
- };
687
- const onDocumentPointerDown = (event) => {
688
- const path = typeof event.composedPath === "function" ? event.composedPath() : [];
689
- const inElement = (element) => Boolean(
690
- element && (path.includes(element) || event.target instanceof Node && element.contains(event.target))
691
- );
692
- const inTableUi = inElement(tableToolbarShell.value) || inElement(tableOverlayShell.value) || inElement(tableMenuShell.value) || inElement(mobileToolbarShell.value);
693
- tableInteractionActive = inTableUi;
694
- if (inTableUi) setTimeout(() => {
695
- tableInteractionActive = false;
696
- }, 0);
697
- if ((tableUi.toolbarOpen || tableUi.overlayOpen || tableUi.menuOpen) && !inElement(root.value) && !inTableUi) {
698
- closeTableUi();
699
- }
700
- };
701
- const onResizePointerDown = (event) => {
702
- tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
703
- };
704
- const onResizePointerUp = () => {
705
- if (!tableResizeActive) return;
706
- tableResizeActive = false;
707
- requestAnimationFrame(() => {
708
- const instance = editor.value;
709
- const table = instance ? selectedTableElement(instance) : null;
710
- if (instance && table) normalizeActiveTableColumnWidths(instance, table);
711
- nextTick(updateTableUi);
712
- });
713
- };
714
- const imageForElement = (element) => {
715
- const instance = editor.value;
716
- if (!instance) return null;
717
- try {
718
- const position = instance.view.posAtDOM(element, 0);
719
- const node = instance.state.doc.nodeAt(position);
720
- return node?.type.name === "image" ? imageDescriptorFromAttrs(node.attrs) : null;
721
- } catch {
722
- return null;
723
- }
724
- };
725
- const imageElementForEvent = (event) => {
726
- const element = event.target instanceof Element ? event.target.closest("img[data-looma-image]") : null;
727
- return element && root.value?.contains(element) ? element : null;
728
- };
729
- const activateImage = (element, trigger) => {
730
- const image = imageForElement(element);
731
- if (image) emit("imageActivate", { ...image, trigger });
732
- };
733
- const onImageClick = (event) => {
734
- const element = imageElementForEvent(event);
735
- if (element && !props.editable) activateImage(element, "pointer");
736
- };
737
- const onImageDoubleClick = (event) => {
738
- const element = imageElementForEvent(event);
739
- if (element && props.editable) activateImage(element, "pointer");
740
- };
741
- const onImageKeyDown = (event) => {
742
- if (event.key !== "Enter" && event.key !== " ") return;
743
- const element = imageElementForEvent(event);
744
- if (!element) return;
745
- event.preventDefault();
746
- activateImage(element, "keyboard");
747
- };
748
- const onImageError = (event) => {
749
- const element = imageElementForEvent(event);
750
- if (!element || element.dataset.loomaRendition !== "true") return;
751
- const image = imageForElement(element);
752
- if (!image) return;
753
- const instance = editor.value;
754
- if (!instance || !imageDelivery.markRenditionFailed(instance, image.src)) return;
755
- emit("imageRenditionError", { ...image, trigger: "programmatic" });
756
- };
757
- onMounted(() => {
758
- updateMobileViewport();
759
- window.addEventListener("resize", onViewportChange);
760
- window.addEventListener("scroll", onViewportChange, true);
761
- window.visualViewport?.addEventListener("resize", onViewportChange);
762
- window.visualViewport?.addEventListener("scroll", onViewportChange);
763
- document.addEventListener("pointerdown", onDocumentPointerDown, true);
764
- document.addEventListener("pointerdown", onResizePointerDown, true);
765
- document.addEventListener("pointerup", onResizePointerUp, true);
766
- root.value?.addEventListener("error", onImageError, true);
767
- });
768
- onBeforeUnmount(() => {
769
- window.removeEventListener("resize", onViewportChange);
770
- window.removeEventListener("scroll", onViewportChange, true);
771
- window.visualViewport?.removeEventListener("resize", onViewportChange);
772
- window.visualViewport?.removeEventListener("scroll", onViewportChange);
773
- document.removeEventListener("pointerdown", onDocumentPointerDown, true);
774
- document.removeEventListener("pointerdown", onResizePointerDown, true);
775
- document.removeEventListener("pointerup", onResizePointerUp, true);
776
- root.value?.removeEventListener("error", onImageError, true);
777
- unbindEditorUi(editor.value);
778
- if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
779
- if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
780
- });
781
- const insertImage = async (file) => {
782
- if (!props.uploadImage || !editor.value) return;
783
- uploading.value = true;
784
- try {
785
- const result = await props.uploadImage(file);
786
- const image = typeof result === "string" ? { url: result } : result;
787
- const width = normalizeImageDimension(image.width);
788
- const height = normalizeImageDimension(image.height);
789
- editor.value.chain().focus().insertContent(imageUploadContent({
790
- src: image.url,
791
- alt: image.alt ?? file.name,
792
- ...width ? { width } : {},
793
- ...height ? { height } : {},
794
- ...image.responsive === true ? { responsive: true } : {}
795
- })).run();
796
- failedUpload.value = null;
797
- } catch (error) {
798
- failedUpload.value = { file };
799
- emit("uploadError", error, file);
800
- } finally {
801
- uploading.value = false;
802
- }
803
- };
804
- const onFileChange = async (event) => {
805
- const input = event.target;
806
- const files = Array.from(input.files ?? []);
807
- input.value = "";
808
- for (const file of files) await insertImage(file);
809
- };
810
- const onDrop = async (event) => {
811
- event.preventDefault();
812
- dragOver.value = false;
813
- if (!props.editable) return;
814
- const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
815
- for (const file of files) await insertImage(file);
816
- };
817
- const onContextMenu = (event) => {
818
- const instance = editor.value;
819
- const cell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
820
- if (!props.editable || !instance || !(cell instanceof HTMLElement)) {
821
- tableUi.menuOpen = false;
822
- return;
823
- }
824
- event.preventDefault();
825
- const currentCell = selectedTableCellElement(instance);
826
- if (currentCell !== cell && !cell.classList.contains("selectedCell")) {
827
- instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
828
- }
829
- updateTableUi();
830
- tableUi.menuOpen = true;
831
- tableUi.menuStyle = { top: `${event.clientY}px`, left: `${event.clientX}px` };
832
- };
833
- const runTableAction = (detail) => {
834
- if (!editor.value) return;
835
- handleTableAction(editor.value, detail);
836
- tableUi.menuOpen = false;
837
- nextTick(updateTableUi);
838
- };
839
- const runOverlayAction = (detail) => {
840
- const instance = editor.value;
841
- if (!instance) return;
842
- const table = overlayTableElement ?? selectedTableElement(instance);
843
- if (!table) return;
844
- const targetCell = "rowIndex" in detail ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) : resolveTableCellAt(
845
- table,
846
- detail.action.startsWith("add-row") ? Math.max(0, detail.boundaryIndex - 1) : 0,
847
- detail.action.startsWith("add-column") ? Math.max(0, detail.boundaryIndex - 1) : 0
848
- );
849
- if (targetCell && selectedTableElement(instance) !== table) {
850
- instance.chain().focus().setTextSelection(instance.view.posAtDOM(targetCell, 0) + 1).run();
851
- }
852
- if (detail.action === "open-cell-menu") {
853
- const selectedCell = selectedTableCellElement(instance);
854
- const cell = table ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) ?? selectedCell : selectedCell;
855
- if (cell && selectedCell !== cell && !cell.classList.contains("selectedCell")) {
856
- instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
857
- } else {
858
- instance.commands.focus();
859
- }
860
- updateTableUi();
861
- tableUi.menuStyle = {
862
- top: `${detail.anchor.bottom + 4}px`,
863
- left: `${detail.anchor.right}px`
864
- };
865
- tableUi.menuOpen = true;
866
- return;
867
- }
868
- handleTableOverlayAction(instance, detail);
869
- nextTick(updateTableUi);
870
- };
871
- const onEditorPointerOver = (event) => {
872
- const cell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
873
- if (!cell) return;
874
- if (cell === hoveredTableCell) return;
875
- hoveredTableCell = cell;
876
- if (tablePointerFrame !== null) return;
877
- tablePointerFrame = requestAnimationFrame(() => {
878
- tablePointerFrame = null;
879
- updateTableUi();
880
- });
881
- };
882
- const onEditorPointerOut = (event) => {
883
- const fromCell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
884
- if (!fromCell || fromCell !== hoveredTableCell) return;
885
- const next = event.relatedTarget;
886
- const toCell = next instanceof HTMLElement ? next.closest("td, th") : null;
887
- if (toCell || next instanceof Node && tableOverlayShell.value?.contains(next)) return;
888
- hoveredTableCell = null;
889
- nextTick(updateTableUi);
890
- };
891
- const onEditorPointerLeave = (event) => {
892
- const next = event.relatedTarget;
893
- if (next instanceof Node && (tableOverlayShell.value?.contains(next) || tableToolbarShell.value?.contains(next) || tableMenuShell.value?.contains(next))) return;
894
- if (!hoveredTableCell) return;
895
- hoveredTableCell = null;
896
- nextTick(updateTableUi);
897
- };
898
- const onTableOverlayPointerLeave = (event) => {
899
- const next = event.relatedTarget;
900
- if (next instanceof Node && (root.value?.contains(next) || tableToolbarShell.value?.contains(next) || tableMenuShell.value?.contains(next))) return;
901
- if (!hoveredTableCell) return;
902
- hoveredTableCell = null;
903
- nextTick(updateTableUi);
904
- };
905
- const onTableOverlayMouseDown = (event) => {
906
- const action = event.target instanceof Element ? event.target.closest("button[data-action]") : null;
907
- if (action) event.preventDefault();
908
- };
909
- const commandButton = (label, icon, active, disabled, run) => {
910
- const syncNativeDisabled = (vnode) => {
911
- if (!(vnode.el instanceof Element)) return;
912
- const button = vnode.el instanceof HTMLButtonElement ? vnode.el : vnode.el.querySelector("button");
913
- if (button) button.disabled = disabled;
914
- };
915
- return h2(IconButton, {
916
- class: "looma-editor__toolbar-button",
917
- label,
918
- title: label,
919
- size: "sm",
920
- variant: active ? "solid" : "ghost",
921
- disabled,
922
- "data-active": active ? "true" : "false",
923
- onVnodeMounted: syncNativeDisabled,
924
- onVnodeUpdated: syncNativeDisabled,
925
- onClick: run
926
- }, () => loomaIcon(icon));
927
- };
928
- const renderToolbar = (instance, floating = true) => {
929
- const buttons = [
930
- commandButton("Bold", "bold", instance.isActive("bold"), !instance.can().toggleBold(), () => instance.chain().focus().toggleBold().run()),
931
- commandButton("Italic", "italic", instance.isActive("italic"), !instance.can().toggleItalic(), () => instance.chain().focus().toggleItalic().run()),
932
- commandButton("Underline", "underline", instance.isActive("underline"), !instance.can().toggleUnderline(), () => instance.chain().focus().toggleUnderline().run()),
933
- commandButton("Strike", "strikethrough", instance.isActive("strike"), !instance.can().toggleStrike(), () => instance.chain().focus().toggleStrike().run()),
934
- commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run()),
935
- commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run()),
936
- h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
937
- commandButton("Heading 1", "heading-1", instance.isActive("heading", { level: 1 }), false, () => instance.chain().focus().toggleHeading({ level: 1 }).run()),
938
- commandButton("Heading 2", "heading-2", instance.isActive("heading", { level: 2 }), false, () => instance.chain().focus().toggleHeading({ level: 2 }).run()),
939
- commandButton("Heading 3", "heading-3", instance.isActive("heading", { level: 3 }), false, () => instance.chain().focus().toggleHeading({ level: 3 }).run()),
940
- commandButton("Bullet list", "list", instance.isActive("bulletList"), false, () => instance.chain().focus().toggleBulletList().run()),
941
- commandButton("Numbered list", "list-ordered", instance.isActive("orderedList"), false, () => instance.chain().focus().toggleOrderedList().run()),
942
- commandButton("Checklist", "list-todo", instance.isActive("taskList"), false, () => instance.chain().focus().toggleTaskList().run()),
943
- commandButton("Blockquote", "quote", instance.isActive("blockquote"), !instance.can().toggleBlockquote(), () => instance.chain().focus().toggleBlockquote().run()),
944
- commandButton("Code block", "braces", instance.isActive("codeBlock"), !instance.can().toggleCodeBlock(), () => instance.chain().focus().toggleCodeBlock().run()),
945
- commandButton("Divider", "minus", false, !instance.can().setHorizontalRule(), () => instance.chain().focus().setHorizontalRule().run()),
946
- h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
947
- h2(IconButton, {
948
- id: tablePickerAnchorId,
949
- class: "looma-editor__toolbar-button",
950
- label: "Insert table",
951
- title: "Insert table",
952
- size: "sm",
953
- variant: tablePickerOpen.value ? "solid" : "ghost",
954
- "aria-expanded": tablePickerOpen.value ? "true" : "false",
955
- onClick: () => {
956
- tablePickerOpen.value = !tablePickerOpen.value;
957
- }
958
- }, () => loomaIcon("table")),
959
- commandButton(uploading.value ? "Uploading image" : "Insert image", "image", false, uploading.value || !props.uploadImage, () => fileInput.value?.click()),
960
- h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
961
- commandButton("Undo", "undo", false, !instance.can().undo(), () => instance.chain().focus().undo().run()),
962
- commandButton("Redo", "redo", false, !instance.can().redo(), () => instance.chain().focus().redo().run())
963
- ];
964
- return h2(EditorToolbar, floating ? { floating: "" } : {}, () => buttons);
965
- };
966
- const focus = (position = "start") => {
967
- editor.value?.commands.focus(position);
968
- };
969
- expose({ editor, focus });
970
- return () => {
971
- const instance = editor.value;
972
- void editorStateVersion.value;
973
- const tableActions = [
974
- "align-left",
975
- "align-center",
976
- "align-right",
977
- "background-none",
978
- "background-gray",
979
- "background-yellow",
980
- "background-blue",
981
- "background-green",
982
- "background-red",
983
- "clear-cells",
984
- ...tableUi.capabilities.canAddRowBefore ? ["add-row-before"] : [],
985
- ...tableUi.capabilities.canAddRowAfter ? ["add-row-after"] : [],
986
- ...tableUi.capabilities.canAddColumnBefore ? ["add-column-before"] : [],
987
- ...tableUi.capabilities.canAddColumnAfter ? ["add-column-after"] : [],
988
- ...tableUi.capabilities.canMergeCells ? ["merge-cells"] : [],
989
- ...tableUi.capabilities.canSplitCell ? ["split-cell"] : [],
990
- ...tableUi.capabilities.canDeleteRow ? ["delete-row"] : [],
991
- ...tableUi.capabilities.canDeleteColumn ? ["delete-column"] : [],
992
- ...tableUi.capabilities.canDeleteTable ? ["delete-table"] : []
993
- ];
994
- const tableProps = {
995
- open: true,
996
- "cell-alignment": tableUi.alignment,
997
- "cell-background": tableUi.background ?? void 0,
998
- actions: tableActions,
999
- onAction: runTableAction
1000
- };
1001
- return h2("div", {
1002
- ...attrs,
1003
- ref: root,
1004
- class: ["looma-editor", attrs.class, { "looma-editor--readonly": !props.editable, "looma-editor--drag-over": dragOver.value }],
1005
- onContextmenu: onContextMenu,
1006
- onPointerover: onEditorPointerOver,
1007
- onPointerout: onEditorPointerOut,
1008
- onPointerleave: onEditorPointerLeave,
1009
- onDragover: (event) => {
1010
- event.preventDefault();
1011
- if (props.editable && Array.from(event.dataTransfer?.types ?? []).includes("Files")) dragOver.value = true;
1012
- },
1013
- onDragleave: () => {
1014
- if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
1015
- dragLeaveTimer = setTimeout(() => {
1016
- dragOver.value = false;
1017
- }, 100);
1018
- },
1019
- onDrop,
1020
- onClick: onImageClick,
1021
- onDblclick: onImageDoubleClick,
1022
- onKeydown: onImageKeyDown
1023
- }, [
1024
- instance && props.editable && !mobile.value && props.toolbarMode === "bubble" ? h2(BubbleMenu, {
1025
- editor: instance,
1026
- pluginKey: "looma-text-formatting-menu",
1027
- shouldShow: ({ editor: menuEditor, from, to }) => shouldShowTextFormattingToolbar(menuEditor, from, to),
1028
- tippyOptions: {
1029
- appendTo: () => root.value ?? document.body,
1030
- duration: 100,
1031
- maxWidth: "none",
1032
- placement: "top"
1033
- }
1034
- }, { default: () => renderToolbar(instance) }) : null,
1035
- instance && props.editable && !mobile.value && props.toolbarMode === "sticky" ? h2("div", {
1036
- class: "looma-editor__sticky-toolbar-shell",
1037
- role: "toolbar",
1038
- "aria-label": "Editor tools"
1039
- }, [renderToolbar(instance, false)]) : null,
1040
- instance ? h2(EditorContent, { editor: instance }) : null,
1041
- instance && props.editable && mobile.value && editorFocused.value ? h2("div", {
1042
- ref: mobileToolbarShell,
1043
- class: "looma-editor__mobile-toolbar-shell",
1044
- "data-mode": mobileToolbarMode.value,
1045
- style: mobileToolbarStyle.value,
1046
- role: "toolbar",
1047
- "aria-label": mobileToolbarMode.value === "table" ? "Table editing" : "Text formatting"
1048
- }, mobileToolbarMode.value === "table" ? [
1049
- h2("button", {
1050
- class: "looma-editor__mobile-toolbar-back",
1051
- type: "button",
1052
- "aria-label": "Formatting tools",
1053
- onClick: () => {
1054
- mobileTableControlsDismissed = true;
1055
- mobileToolbarMode.value = "formatting";
1056
- editor.value?.commands.focus();
1057
- nextTick(updateMobileViewport);
1058
- }
1059
- }, [loomaIcon("chevron-left"), h2("span", "Formatting")]),
1060
- h2(EditorTableToolbar, tableProps)
1061
- ] : [renderToolbar(instance)]) : null,
1062
- h2("input", {
1063
- ref: fileInput,
1064
- class: "looma-editor__file-input",
1065
- type: "file",
1066
- accept: "image/jpeg,image/png,image/gif,image/webp,image/svg+xml",
1067
- multiple: true,
1068
- tabindex: -1,
1069
- "aria-hidden": "true",
1070
- onChange: onFileChange
1071
- }),
1072
- failedUpload.value ? h2("div", {
1073
- class: "looma-editor__upload-error",
1074
- role: "alert"
1075
- }, [
1076
- h2("span", "Couldn\u2019t upload image."),
1077
- h2("button", {
1078
- type: "button",
1079
- disabled: uploading.value,
1080
- onClick: () => {
1081
- const attempt = failedUpload.value;
1082
- if (attempt) void insertImage(attempt.file);
1083
- }
1084
- }, uploading.value ? "Retrying\u2026" : "Retry")
1085
- ]) : null,
1086
- dragOver.value ? h2("div", { class: "looma-editor__drop-overlay", "aria-hidden": "true" }, [
1087
- loomaIcon("image"),
1088
- h2("span", "Drop image to upload")
1089
- ]) : null,
1090
- h2(Popover, {
1091
- class: "looma-editor__table-picker-popover",
1092
- open: tablePickerOpen.value,
1093
- for: tablePickerAnchorId,
1094
- placement: mobile.value ? "top-start" : "bottom-start",
1095
- onClose: () => {
1096
- tablePickerOpen.value = false;
1097
- }
1098
- }, () => [h2(EditorInsertTableGrid, {
1099
- open: true,
1100
- onInsert: (detail) => {
1101
- instance?.chain().focus().insertTable(detail).run();
1102
- tablePickerOpen.value = false;
1103
- }
1104
- })]),
1105
- slash.active && slash.items.length > 0 ? h2(EditorSlashMenu, {
1106
- open: true,
1107
- query: slash.query,
1108
- items: managedSlashMenuItems(slash.items),
1109
- selectedIndex: slash.selectedIndex,
1110
- anchorRect: managedMenuAnchorRect(slash.rect),
1111
- onHighlight: ({ index }) => {
1112
- slash.selectedIndex = index;
1113
- },
1114
- onSelect: ({ index }) => {
1115
- slash.select?.(index);
1116
- }
1117
- }) : null,
1118
- mention.active && (mention.loading || mention.items.length > 0) ? h2(EditorMentionMenu, {
1119
- id: mentionMenuId,
1120
- open: true,
1121
- query: mention.query,
1122
- items: mention.items,
1123
- selectedIndex: mention.selectedIndex,
1124
- anchorRect: managedMenuAnchorRect(mention.rect),
1125
- loading: mention.loading,
1126
- onHighlight: ({ index }) => {
1127
- mention.selectedIndex = index;
1128
- mention.highlight?.(index);
1129
- },
1130
- onSelect: ({ index }) => {
1131
- mention.select?.(index);
1132
- }
1133
- }) : null,
1134
- tableUi.toolbarOpen ? h2("div", {
1135
- ref: tableToolbarShell,
1136
- class: "looma-editor__table-toolbar-shell",
1137
- style: tableUi.toolbarStyle
1138
- }, [h2(EditorTableToolbar, tableProps)]) : null,
1139
- tableUi.overlayOpen ? h2("div", {
1140
- ref: tableOverlayShell,
1141
- class: "looma-editor__table-overlay-shell",
1142
- style: tableUi.overlayStyle,
1143
- onMousedown: onTableOverlayMouseDown,
1144
- onPointerleave: onTableOverlayPointerLeave
1145
- }, [h2(EditorTableOverlay, {
1146
- open: true,
1147
- rows: tableUi.rows,
1148
- cols: tableUi.cols,
1149
- geometry: tableUi.geometry,
1150
- onAction: runOverlayAction
1151
- })]) : null,
1152
- tableUi.menuOpen ? h2("div", {
1153
- ref: tableMenuShell,
1154
- class: "looma-editor__table-menu-shell",
1155
- style: tableUi.menuStyle
1156
- }, [h2(EditorTableContextMenu, tableProps)]) : null
1157
- ]);
1158
- };
1159
- }
243
+ /**
244
+ * Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
245
+ * with declarative menus, image delivery, and table interaction chrome.
246
+ *
247
+ * @ownership The component owns its Tiptap instance and transient UI state;
248
+ * callers own controlled `modelValue`, extension instances, and host services.
249
+ * @lifecycle Mount creates one editor and browser listener set, prop watchers
250
+ * synchronize external state, and unmount cancels frames/listeners and destroys
251
+ * the editor-owned resources.
252
+ * @failure Upload and responsive-rendition failures are emitted to the host;
253
+ * durable document content is retained or left unchanged for recovery.
254
+ */
255
+ var LoomaEditor = defineComponent({
256
+ name: "LoomaEditor",
257
+ inheritAttrs: false,
258
+ props: {
259
+ modelValue: {
260
+ type: Object,
261
+ default: () => ({ ...EMPTY_DOCUMENT })
262
+ },
263
+ editable: {
264
+ type: Boolean,
265
+ default: true
266
+ },
267
+ placeholder: {
268
+ type: String,
269
+ default: "Type “/” for commands, or start writing…"
270
+ },
271
+ extensions: {
272
+ type: Array,
273
+ default: () => []
274
+ },
275
+ mentionItems: {
276
+ type: Array,
277
+ default: () => []
278
+ },
279
+ mentionProvider: {
280
+ type: Function,
281
+ default: void 0
282
+ },
283
+ mentionLimit: {
284
+ type: Number,
285
+ default: DEFAULT_MENTION_RESULT_LIMIT
286
+ },
287
+ uploadImage: {
288
+ type: Function,
289
+ default: void 0
290
+ },
291
+ resolveImageAttributes: {
292
+ type: Function,
293
+ default: void 0
294
+ },
295
+ toolbarMode: {
296
+ type: String,
297
+ default: "bubble"
298
+ }
299
+ },
300
+ emits: {
301
+ "update:modelValue": (_value) => true,
302
+ update: (_value) => true,
303
+ ready: (_editor) => true,
304
+ uploadError: (_error, _file) => true,
305
+ imageActivate: (_detail) => true,
306
+ imageRenditionError: (_detail) => true
307
+ },
308
+ setup(props, { attrs, emit, expose }) {
309
+ const root = ref(null);
310
+ const fileInput = ref(null);
311
+ const tablePickerAnchorId = `looma-editor-table-picker-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
312
+ const tableToolbarShell = ref(null);
313
+ const tableOverlayShell = ref(null);
314
+ const tableMenuShell = ref(null);
315
+ const mobileToolbarShell = ref(null);
316
+ const dragOver = ref(false);
317
+ const uploading = ref(false);
318
+ const failedUpload = ref(null);
319
+ const tablePickerOpen = ref(false);
320
+ const mobileToolbarStyle = ref({});
321
+ const mobile = ref(typeof window !== "undefined" && window.innerWidth <= 767);
322
+ const editorFocused = ref(false);
323
+ const editorStateVersion = ref(0);
324
+ const mobileToolbarMode = ref("formatting");
325
+ let dragLeaveTimer = null;
326
+ let tableResizeActive = false;
327
+ let tableInteractionActive = false;
328
+ let mobileTableControlsDismissed = false;
329
+ let hoveredTableCell = null;
330
+ let overlayTableElement = null;
331
+ let tablePointerFrame = null;
332
+ const slash = reactive({
333
+ active: false,
334
+ items: [],
335
+ selectedIndex: 0,
336
+ query: "",
337
+ rect: null,
338
+ select: null
339
+ });
340
+ const mention = reactive({
341
+ active: false,
342
+ items: [],
343
+ selectedIndex: 0,
344
+ query: "",
345
+ rect: null,
346
+ loading: false,
347
+ highlight: null,
348
+ select: null
349
+ });
350
+ const tableUi = reactive({
351
+ toolbarOpen: false,
352
+ overlayOpen: false,
353
+ menuOpen: false,
354
+ alignment: "left",
355
+ background: null,
356
+ rows: 0,
357
+ cols: 0,
358
+ geometry: null,
359
+ toolbarStyle: {},
360
+ overlayStyle: {},
361
+ menuStyle: {},
362
+ capabilities: { ...EMPTY_CAPABILITIES }
363
+ });
364
+ const slashExtension = createLoomaSlashCommandExtension({
365
+ onOpenImagePicker: () => fileInput.value?.click(),
366
+ onStateChange: (state) => {
367
+ Object.assign(slash, state);
368
+ }
369
+ });
370
+ const mentionMenuId = `looma-editor-mention-menu-${++editorInstanceSequence}`;
371
+ const mentionExtension = props.extensions.some((extension) => extension.name === "mention") ? null : createLoomaMentionExtension({
372
+ limit: props.mentionLimit,
373
+ menuId: mentionMenuId,
374
+ items: async (query, context) => {
375
+ if (props.mentionProvider) return await props.mentionProvider(query, context);
376
+ return props.mentionItems;
377
+ },
378
+ onStateChange: (state) => {
379
+ Object.assign(mention, state);
380
+ }
381
+ });
382
+ const imageDelivery = createLoomaImageDeliveryController({ resolveAttributes: () => props.resolveImageAttributes });
383
+ let lastFocusedSelection = null;
384
+ const rememberSelection = (instance) => {
385
+ if (!instance.isFocused) return;
386
+ const selection = instance.view.state.selection;
387
+ lastFocusedSelection = {
388
+ bookmark: selection.getBookmark(),
389
+ from: selection.from,
390
+ to: selection.to,
391
+ text: selection instanceof TextSelection
392
+ };
393
+ };
394
+ const editor = useEditor({
395
+ extensions: [
396
+ ...getDefaultEditorExtensions({
397
+ placeholder: props.placeholder,
398
+ mention: mentionExtension ?? false
399
+ }),
400
+ imageDelivery.extension,
401
+ slashExtension,
402
+ ...props.extensions
403
+ ],
404
+ content: props.modelValue,
405
+ editable: props.editable,
406
+ onCreate: ({ editor: instance }) => emit("ready", instance),
407
+ onFocus: ({ editor: instance }) => rememberSelection(instance),
408
+ onSelectionUpdate: ({ editor: instance }) => rememberSelection(instance),
409
+ onTransaction: () => {
410
+ editorStateVersion.value += 1;
411
+ },
412
+ onUpdate: ({ editor: instance }) => {
413
+ const value = instance.getJSON();
414
+ emit("update:modelValue", value);
415
+ emit("update", value);
416
+ }
417
+ });
418
+ watch(() => props.editable, (editable) => editor.value?.setEditable(editable));
419
+ watch(() => props.resolveImageAttributes, () => {
420
+ const instance = editor.value;
421
+ if (instance) imageDelivery.reset(instance);
422
+ });
423
+ watch(() => props.modelValue, (value) => {
424
+ const instance = editor.value;
425
+ if (!instance || sameDocument(instance.getJSON(), value)) return;
426
+ const wasFocused = instance.isFocused;
427
+ const previousSelection = lastFocusedSelection ?? {
428
+ bookmark: instance.view.state.selection.getBookmark(),
429
+ from: instance.view.state.selection.from,
430
+ to: instance.view.state.selection.to,
431
+ text: instance.view.state.selection instanceof TextSelection
432
+ };
433
+ if (!wasFocused) {
434
+ instance.commands.setContent(value, false);
435
+ return;
436
+ }
437
+ instance.chain().setContent(value, false).command(({ tr }) => {
438
+ try {
439
+ tr.setSelection(previousSelection.text ? TextSelection.create(tr.doc, previousSelection.from, previousSelection.to) : previousSelection.bookmark.resolve(tr.doc));
440
+ } catch {
441
+ const max = tr.doc.content.size;
442
+ const from = Math.max(1, Math.min(previousSelection.from, max));
443
+ const to = Math.max(from, Math.min(previousSelection.to, max));
444
+ tr.setSelection(TextSelection.create(tr.doc, from, to));
445
+ }
446
+ return true;
447
+ }).run();
448
+ instance.view.focus();
449
+ rememberSelection(instance);
450
+ }, { deep: true });
451
+ const closeTableUi = () => {
452
+ tableUi.toolbarOpen = false;
453
+ tableUi.overlayOpen = false;
454
+ tableUi.menuOpen = false;
455
+ tableUi.alignment = "left";
456
+ tableUi.background = null;
457
+ tableUi.geometry = null;
458
+ tableUi.capabilities = { ...EMPTY_CAPABILITIES };
459
+ overlayTableElement = null;
460
+ mobileTableControlsDismissed = false;
461
+ mobileToolbarMode.value = "formatting";
462
+ };
463
+ const updateMobileViewport = () => {
464
+ mobile.value = window.innerWidth <= 767;
465
+ if (!mobile.value) {
466
+ mobileToolbarStyle.value = {};
467
+ return;
468
+ }
469
+ nextTick(() => {
470
+ const viewport = getVisualViewportRect(window);
471
+ const toolbarHeight = mobileToolbarShell.value?.getBoundingClientRect().height || 56;
472
+ mobileToolbarStyle.value = {
473
+ left: `${viewport.left}px`,
474
+ top: `${Math.max(viewport.top, viewport.bottom - toolbarHeight)}px`,
475
+ width: `${viewport.width}px`
476
+ };
477
+ });
478
+ };
479
+ const updateTableUi = () => {
480
+ const instance = editor.value;
481
+ if (!instance || !props.editable || !instance.isFocused && !tableInteractionActive && !hoveredTableCell) {
482
+ closeTableUi();
483
+ return;
484
+ }
485
+ const state = getActiveTableUiState(instance);
486
+ const selectedTable = selectedTableElement(instance);
487
+ const table = hoveredTableCell?.closest("table") ?? null ?? selectedTable;
488
+ if (!table) {
489
+ closeTableUi();
490
+ return;
491
+ }
492
+ const selectedTableIsActive = state.active && selectedTable === table;
493
+ overlayTableElement = table;
494
+ if (selectedTableIsActive && !mobileTableControlsDismissed) mobileToolbarMode.value = "table";
495
+ const rect = table.getBoundingClientRect();
496
+ const maxToolbarWidth = Math.min(420, window.innerWidth - 24);
497
+ const toolbarLeft = Math.min(Math.max(12, rect.left + (rect.width - maxToolbarWidth) / 2), Math.max(12, window.innerWidth - maxToolbarWidth - 12));
498
+ tableUi.toolbarOpen = selectedTableIsActive && state.showToolbar && !mobile.value;
499
+ tableUi.overlayOpen = !mobile.value;
500
+ tableUi.alignment = selectedTableIsActive ? state.cellAlignment : "left";
501
+ tableUi.background = selectedTableIsActive ? state.cellBackground : null;
502
+ tableUi.capabilities = selectedTableIsActive ? state.capabilities : { ...EMPTY_CAPABILITIES };
503
+ const selectedCell = selectedTableIsActive ? selectedTableCellElement(instance) : null;
504
+ const geometry = measureTableOverlayGeometry(table, selectedCell, hoveredTableCell && table.contains(hoveredTableCell) ? hoveredTableCell : null);
505
+ tableUi.rows = Math.max(0, geometry.rowBoundaries.length - 1);
506
+ tableUi.cols = Math.max(0, geometry.columnBoundaries.length - 1);
507
+ tableUi.geometry = geometry;
508
+ tableUi.toolbarStyle = {
509
+ top: `${rect.top > 76 ? rect.top - 52 : rect.bottom + 12}px`,
510
+ left: `${toolbarLeft}px`,
511
+ maxWidth: `${maxToolbarWidth}px`
512
+ };
513
+ tableUi.overlayStyle = {
514
+ top: `${rect.top}px`,
515
+ left: `${rect.left}px`,
516
+ width: `${rect.width}px`,
517
+ height: `${rect.height}px`
518
+ };
519
+ };
520
+ const onEditorFocus = () => {
521
+ editorFocused.value = true;
522
+ updateTableUi();
523
+ updateMobileViewport();
524
+ };
525
+ const onEditorBlur = () => {
526
+ updateTableUi();
527
+ setTimeout(() => {
528
+ if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
529
+ }, 0);
530
+ };
531
+ const bindEditorUi = (instance) => {
532
+ if (!instance) return;
533
+ instance.on("selectionUpdate", updateTableUi);
534
+ instance.on("transaction", updateTableUi);
535
+ instance.on("focus", onEditorFocus);
536
+ instance.on("blur", onEditorBlur);
537
+ nextTick(updateTableUi);
538
+ };
539
+ const unbindEditorUi = (instance) => {
540
+ if (!instance) return;
541
+ instance.off("selectionUpdate", updateTableUi);
542
+ instance.off("transaction", updateTableUi);
543
+ instance.off("focus", onEditorFocus);
544
+ instance.off("blur", onEditorBlur);
545
+ };
546
+ watch(editor, (instance, previous) => {
547
+ unbindEditorUi(previous);
548
+ bindEditorUi(instance);
549
+ }, { immediate: true });
550
+ const onViewportChange = () => {
551
+ updateMobileViewport();
552
+ updateTableUi();
553
+ tableUi.menuOpen = false;
554
+ };
555
+ const onDocumentPointerDown = (event) => {
556
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
557
+ const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
558
+ const inTableUi = inElement(tableToolbarShell.value) || inElement(tableOverlayShell.value) || inElement(tableMenuShell.value) || inElement(mobileToolbarShell.value);
559
+ tableInteractionActive = inTableUi;
560
+ if (inTableUi) setTimeout(() => {
561
+ tableInteractionActive = false;
562
+ }, 0);
563
+ if ((tableUi.toolbarOpen || tableUi.overlayOpen || tableUi.menuOpen) && !inElement(root.value) && !inTableUi) closeTableUi();
564
+ };
565
+ const onResizePointerDown = (event) => {
566
+ tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
567
+ };
568
+ const onResizePointerUp = () => {
569
+ if (!tableResizeActive) return;
570
+ tableResizeActive = false;
571
+ requestAnimationFrame(() => {
572
+ const instance = editor.value;
573
+ const table = instance ? selectedTableElement(instance) : null;
574
+ if (instance && table) normalizeActiveTableColumnWidths(instance, table);
575
+ nextTick(updateTableUi);
576
+ });
577
+ };
578
+ const imageForElement = (element) => {
579
+ const instance = editor.value;
580
+ if (!instance) return null;
581
+ try {
582
+ const position = instance.view.posAtDOM(element, 0);
583
+ const node = instance.state.doc.nodeAt(position);
584
+ return node?.type.name === "image" ? imageDescriptorFromAttrs(node.attrs) : null;
585
+ } catch {
586
+ return null;
587
+ }
588
+ };
589
+ const imageElementForEvent = (event) => {
590
+ const element = event.target instanceof Element ? event.target.closest("img[data-looma-image]") : null;
591
+ return element && root.value?.contains(element) ? element : null;
592
+ };
593
+ const activateImage = (element, trigger) => {
594
+ const image = imageForElement(element);
595
+ if (image) emit("imageActivate", {
596
+ ...image,
597
+ trigger
598
+ });
599
+ };
600
+ const onImageClick = (event) => {
601
+ const element = imageElementForEvent(event);
602
+ if (element && !props.editable) activateImage(element, "pointer");
603
+ };
604
+ const onImageDoubleClick = (event) => {
605
+ const element = imageElementForEvent(event);
606
+ if (element && props.editable) activateImage(element, "pointer");
607
+ };
608
+ const onImageKeyDown = (event) => {
609
+ if (event.key !== "Enter" && event.key !== " ") return;
610
+ const element = imageElementForEvent(event);
611
+ if (!element) return;
612
+ event.preventDefault();
613
+ activateImage(element, "keyboard");
614
+ };
615
+ const onImageError = (event) => {
616
+ const element = imageElementForEvent(event);
617
+ if (!element || element.dataset.loomaRendition !== "true") return;
618
+ const image = imageForElement(element);
619
+ if (!image) return;
620
+ const instance = editor.value;
621
+ if (!instance || !imageDelivery.markRenditionFailed(instance, image.src)) return;
622
+ emit("imageRenditionError", {
623
+ ...image,
624
+ trigger: "programmatic"
625
+ });
626
+ };
627
+ onMounted(() => {
628
+ updateMobileViewport();
629
+ window.addEventListener("resize", onViewportChange);
630
+ window.addEventListener("scroll", onViewportChange, true);
631
+ window.visualViewport?.addEventListener("resize", onViewportChange);
632
+ window.visualViewport?.addEventListener("scroll", onViewportChange);
633
+ document.addEventListener("pointerdown", onDocumentPointerDown, true);
634
+ document.addEventListener("pointerdown", onResizePointerDown, true);
635
+ document.addEventListener("pointerup", onResizePointerUp, true);
636
+ root.value?.addEventListener("error", onImageError, true);
637
+ });
638
+ onBeforeUnmount(() => {
639
+ window.removeEventListener("resize", onViewportChange);
640
+ window.removeEventListener("scroll", onViewportChange, true);
641
+ window.visualViewport?.removeEventListener("resize", onViewportChange);
642
+ window.visualViewport?.removeEventListener("scroll", onViewportChange);
643
+ document.removeEventListener("pointerdown", onDocumentPointerDown, true);
644
+ document.removeEventListener("pointerdown", onResizePointerDown, true);
645
+ document.removeEventListener("pointerup", onResizePointerUp, true);
646
+ root.value?.removeEventListener("error", onImageError, true);
647
+ unbindEditorUi(editor.value);
648
+ if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
649
+ if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
650
+ });
651
+ const insertImage = async (file) => {
652
+ if (!props.uploadImage || !editor.value) return;
653
+ uploading.value = true;
654
+ try {
655
+ const result = await props.uploadImage(file);
656
+ const image = typeof result === "string" ? { url: result } : result;
657
+ const width = normalizeImageDimension(image.width);
658
+ const height = normalizeImageDimension(image.height);
659
+ editor.value.chain().focus().insertContent(imageUploadContent({
660
+ src: image.url,
661
+ alt: image.alt ?? file.name,
662
+ ...width ? { width } : {},
663
+ ...height ? { height } : {},
664
+ ...image.responsive === true ? { responsive: true } : {}
665
+ })).run();
666
+ failedUpload.value = null;
667
+ } catch (error) {
668
+ failedUpload.value = { file };
669
+ emit("uploadError", error, file);
670
+ } finally {
671
+ uploading.value = false;
672
+ }
673
+ };
674
+ const onFileChange = async (event) => {
675
+ const input = event.target;
676
+ const files = Array.from(input.files ?? []);
677
+ input.value = "";
678
+ for (const file of files) await insertImage(file);
679
+ };
680
+ const onDrop = async (event) => {
681
+ event.preventDefault();
682
+ dragOver.value = false;
683
+ if (!props.editable) return;
684
+ const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
685
+ for (const file of files) await insertImage(file);
686
+ };
687
+ const onContextMenu = (event) => {
688
+ const instance = editor.value;
689
+ const cell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
690
+ if (!props.editable || !instance || !(cell instanceof HTMLElement)) {
691
+ tableUi.menuOpen = false;
692
+ return;
693
+ }
694
+ event.preventDefault();
695
+ if (selectedTableCellElement(instance) !== cell && !cell.classList.contains("selectedCell")) instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
696
+ updateTableUi();
697
+ tableUi.menuOpen = true;
698
+ tableUi.menuStyle = {
699
+ top: `${event.clientY}px`,
700
+ left: `${event.clientX}px`
701
+ };
702
+ };
703
+ const runTableAction = (detail) => {
704
+ if (!editor.value) return;
705
+ handleTableAction(editor.value, detail);
706
+ tableUi.menuOpen = false;
707
+ nextTick(updateTableUi);
708
+ };
709
+ const runOverlayAction = (detail) => {
710
+ const instance = editor.value;
711
+ if (!instance) return;
712
+ const table = overlayTableElement ?? selectedTableElement(instance);
713
+ if (!table) return;
714
+ const targetCell = "rowIndex" in detail ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) : resolveTableCellAt(table, detail.action.startsWith("add-row") ? Math.max(0, detail.boundaryIndex - 1) : 0, detail.action.startsWith("add-column") ? Math.max(0, detail.boundaryIndex - 1) : 0);
715
+ if (targetCell && selectedTableElement(instance) !== table) instance.chain().focus().setTextSelection(instance.view.posAtDOM(targetCell, 0) + 1).run();
716
+ if (detail.action === "open-cell-menu") {
717
+ const selectedCell = selectedTableCellElement(instance);
718
+ const cell = table ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) ?? selectedCell : selectedCell;
719
+ if (cell && selectedCell !== cell && !cell.classList.contains("selectedCell")) instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
720
+ else instance.commands.focus();
721
+ updateTableUi();
722
+ tableUi.menuStyle = {
723
+ top: `${detail.anchor.bottom + 4}px`,
724
+ left: `${detail.anchor.right}px`
725
+ };
726
+ tableUi.menuOpen = true;
727
+ return;
728
+ }
729
+ handleTableOverlayAction(instance, detail);
730
+ nextTick(updateTableUi);
731
+ };
732
+ const onEditorPointerOver = (event) => {
733
+ const cell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
734
+ if (!cell) return;
735
+ if (cell === hoveredTableCell) return;
736
+ hoveredTableCell = cell;
737
+ if (tablePointerFrame !== null) return;
738
+ tablePointerFrame = requestAnimationFrame(() => {
739
+ tablePointerFrame = null;
740
+ updateTableUi();
741
+ });
742
+ };
743
+ const onEditorPointerOut = (event) => {
744
+ const fromCell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
745
+ if (!fromCell || fromCell !== hoveredTableCell) return;
746
+ const next = event.relatedTarget;
747
+ if ((next instanceof HTMLElement ? next.closest("td, th") : null) || next instanceof Node && tableOverlayShell.value?.contains(next)) return;
748
+ hoveredTableCell = null;
749
+ nextTick(updateTableUi);
750
+ };
751
+ const onEditorPointerLeave = (event) => {
752
+ const next = event.relatedTarget;
753
+ if (next instanceof Node && (tableOverlayShell.value?.contains(next) || tableToolbarShell.value?.contains(next) || tableMenuShell.value?.contains(next))) return;
754
+ if (!hoveredTableCell) return;
755
+ hoveredTableCell = null;
756
+ nextTick(updateTableUi);
757
+ };
758
+ const onTableOverlayPointerLeave = (event) => {
759
+ const next = event.relatedTarget;
760
+ if (next instanceof Node && (root.value?.contains(next) || tableToolbarShell.value?.contains(next) || tableMenuShell.value?.contains(next))) return;
761
+ if (!hoveredTableCell) return;
762
+ hoveredTableCell = null;
763
+ nextTick(updateTableUi);
764
+ };
765
+ const onTableOverlayMouseDown = (event) => {
766
+ if (event.target instanceof Element ? event.target.closest("button[data-action]") : null) event.preventDefault();
767
+ };
768
+ const commandButton = (label, icon, active, disabled, run) => {
769
+ const syncNativeDisabled = (vnode) => {
770
+ if (!(vnode.el instanceof Element)) return;
771
+ const button = vnode.el instanceof HTMLButtonElement ? vnode.el : vnode.el.querySelector("button");
772
+ if (button) button.disabled = disabled;
773
+ };
774
+ return h(IconButton, {
775
+ class: "looma-editor__toolbar-button",
776
+ label,
777
+ title: label,
778
+ size: "sm",
779
+ variant: active ? "solid" : "ghost",
780
+ disabled,
781
+ "data-active": active ? "true" : "false",
782
+ onVnodeMounted: syncNativeDisabled,
783
+ onVnodeUpdated: syncNativeDisabled,
784
+ onClick: run
785
+ }, () => loomaIcon(icon));
786
+ };
787
+ const renderToolbar = (instance, floating = true) => {
788
+ const buttons = [
789
+ commandButton("Bold", "bold", instance.isActive("bold"), !instance.can().toggleBold(), () => instance.chain().focus().toggleBold().run()),
790
+ commandButton("Italic", "italic", instance.isActive("italic"), !instance.can().toggleItalic(), () => instance.chain().focus().toggleItalic().run()),
791
+ commandButton("Underline", "underline", instance.isActive("underline"), !instance.can().toggleUnderline(), () => instance.chain().focus().toggleUnderline().run()),
792
+ commandButton("Strike", "strikethrough", instance.isActive("strike"), !instance.can().toggleStrike(), () => instance.chain().focus().toggleStrike().run()),
793
+ commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run()),
794
+ commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run()),
795
+ h("span", {
796
+ class: "divider",
797
+ "aria-hidden": "true"
798
+ }),
799
+ commandButton("Heading 1", "heading-1", instance.isActive("heading", { level: 1 }), false, () => instance.chain().focus().toggleHeading({ level: 1 }).run()),
800
+ commandButton("Heading 2", "heading-2", instance.isActive("heading", { level: 2 }), false, () => instance.chain().focus().toggleHeading({ level: 2 }).run()),
801
+ commandButton("Heading 3", "heading-3", instance.isActive("heading", { level: 3 }), false, () => instance.chain().focus().toggleHeading({ level: 3 }).run()),
802
+ commandButton("Bullet list", "list", instance.isActive("bulletList"), false, () => instance.chain().focus().toggleBulletList().run()),
803
+ commandButton("Numbered list", "list-ordered", instance.isActive("orderedList"), false, () => instance.chain().focus().toggleOrderedList().run()),
804
+ commandButton("Checklist", "list-todo", instance.isActive("taskList"), false, () => instance.chain().focus().toggleTaskList().run()),
805
+ commandButton("Blockquote", "quote", instance.isActive("blockquote"), !instance.can().toggleBlockquote(), () => instance.chain().focus().toggleBlockquote().run()),
806
+ commandButton("Code block", "braces", instance.isActive("codeBlock"), !instance.can().toggleCodeBlock(), () => instance.chain().focus().toggleCodeBlock().run()),
807
+ commandButton("Divider", "minus", false, !instance.can().setHorizontalRule(), () => instance.chain().focus().setHorizontalRule().run()),
808
+ h("span", {
809
+ class: "divider",
810
+ "aria-hidden": "true"
811
+ }),
812
+ h(IconButton, {
813
+ id: tablePickerAnchorId,
814
+ class: "looma-editor__toolbar-button",
815
+ label: "Insert table",
816
+ title: "Insert table",
817
+ size: "sm",
818
+ variant: tablePickerOpen.value ? "solid" : "ghost",
819
+ "aria-expanded": tablePickerOpen.value ? "true" : "false",
820
+ onClick: () => {
821
+ tablePickerOpen.value = !tablePickerOpen.value;
822
+ }
823
+ }, () => loomaIcon("table")),
824
+ commandButton(uploading.value ? "Uploading image" : "Insert image", "image", false, uploading.value || !props.uploadImage, () => fileInput.value?.click()),
825
+ h("span", {
826
+ class: "divider",
827
+ "aria-hidden": "true"
828
+ }),
829
+ commandButton("Undo", "undo", false, !instance.can().undo(), () => instance.chain().focus().undo().run()),
830
+ commandButton("Redo", "redo", false, !instance.can().redo(), () => instance.chain().focus().redo().run())
831
+ ];
832
+ return h(EditorToolbar, { floating }, () => buttons);
833
+ };
834
+ const focus = (position = "start") => {
835
+ editor.value?.commands.focus(position);
836
+ };
837
+ expose({
838
+ editor,
839
+ focus
840
+ });
841
+ return () => {
842
+ const instance = editor.value;
843
+ editorStateVersion.value;
844
+ const tableActions = [
845
+ "align-left",
846
+ "align-center",
847
+ "align-right",
848
+ "background-none",
849
+ "background-gray",
850
+ "background-yellow",
851
+ "background-blue",
852
+ "background-green",
853
+ "background-red",
854
+ "clear-cells",
855
+ ...tableUi.capabilities.canAddRowBefore ? ["add-row-before"] : [],
856
+ ...tableUi.capabilities.canAddRowAfter ? ["add-row-after"] : [],
857
+ ...tableUi.capabilities.canAddColumnBefore ? ["add-column-before"] : [],
858
+ ...tableUi.capabilities.canAddColumnAfter ? ["add-column-after"] : [],
859
+ ...tableUi.capabilities.canMergeCells ? ["merge-cells"] : [],
860
+ ...tableUi.capabilities.canSplitCell ? ["split-cell"] : [],
861
+ ...tableUi.capabilities.canDeleteRow ? ["delete-row"] : [],
862
+ ...tableUi.capabilities.canDeleteColumn ? ["delete-column"] : [],
863
+ ...tableUi.capabilities.canDeleteTable ? ["delete-table"] : []
864
+ ];
865
+ const tableProps = {
866
+ open: true,
867
+ cellAlignment: tableUi.alignment,
868
+ cellBackground: tableUi.background ?? void 0,
869
+ actions: tableActions,
870
+ onAction: runTableAction
871
+ };
872
+ return h("div", {
873
+ ...attrs,
874
+ ref: root,
875
+ class: [
876
+ "looma-editor",
877
+ attrs.class,
878
+ {
879
+ "looma-editor--readonly": !props.editable,
880
+ "looma-editor--drag-over": dragOver.value
881
+ }
882
+ ],
883
+ onContextmenu: onContextMenu,
884
+ onPointerover: onEditorPointerOver,
885
+ onPointerout: onEditorPointerOut,
886
+ onPointerleave: onEditorPointerLeave,
887
+ onDragover: (event) => {
888
+ event.preventDefault();
889
+ if (props.editable && Array.from(event.dataTransfer?.types ?? []).includes("Files")) dragOver.value = true;
890
+ },
891
+ onDragleave: () => {
892
+ if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
893
+ dragLeaveTimer = setTimeout(() => {
894
+ dragOver.value = false;
895
+ }, 100);
896
+ },
897
+ onDrop,
898
+ onClick: onImageClick,
899
+ onDblclick: onImageDoubleClick,
900
+ onKeydown: onImageKeyDown
901
+ }, [
902
+ instance && props.editable && !mobile.value && props.toolbarMode === "bubble" ? h(BubbleMenu, {
903
+ editor: instance,
904
+ pluginKey: "looma-text-formatting-menu",
905
+ shouldShow: ({ editor: menuEditor, from, to }) => shouldShowTextFormattingToolbar(menuEditor, from, to),
906
+ tippyOptions: {
907
+ appendTo: () => root.value ?? document.body,
908
+ duration: 100,
909
+ maxWidth: "none",
910
+ placement: "top"
911
+ }
912
+ }, { default: () => renderToolbar(instance) }) : null,
913
+ instance && props.editable && !mobile.value && props.toolbarMode === "sticky" ? h("div", {
914
+ class: "looma-editor__sticky-toolbar-shell",
915
+ role: "toolbar",
916
+ "aria-label": "Editor tools"
917
+ }, [renderToolbar(instance, false)]) : null,
918
+ instance ? h(EditorContent, { editor: instance }) : null,
919
+ instance && props.editable && mobile.value && editorFocused.value ? h("div", {
920
+ ref: mobileToolbarShell,
921
+ class: "looma-editor__mobile-toolbar-shell",
922
+ "data-mode": mobileToolbarMode.value,
923
+ style: mobileToolbarStyle.value,
924
+ role: "toolbar",
925
+ "aria-label": mobileToolbarMode.value === "table" ? "Table editing" : "Text formatting"
926
+ }, mobileToolbarMode.value === "table" ? [h("button", {
927
+ class: "looma-editor__mobile-toolbar-back",
928
+ type: "button",
929
+ "aria-label": "Formatting tools",
930
+ onClick: () => {
931
+ mobileTableControlsDismissed = true;
932
+ mobileToolbarMode.value = "formatting";
933
+ editor.value?.commands.focus();
934
+ nextTick(updateMobileViewport);
935
+ }
936
+ }, [loomaIcon("chevron-left"), h("span", "Formatting")]), h(EditorTableToolbar, tableProps)] : [renderToolbar(instance)]) : null,
937
+ h("input", {
938
+ ref: fileInput,
939
+ class: "looma-editor__file-input",
940
+ type: "file",
941
+ accept: "image/jpeg,image/png,image/gif,image/webp,image/svg+xml",
942
+ multiple: true,
943
+ tabindex: -1,
944
+ "aria-hidden": "true",
945
+ onChange: onFileChange
946
+ }),
947
+ failedUpload.value ? h("div", {
948
+ class: "looma-editor__upload-error",
949
+ role: "alert"
950
+ }, [h("span", "Couldn’t upload image."), h("button", {
951
+ type: "button",
952
+ disabled: uploading.value,
953
+ onClick: () => {
954
+ const attempt = failedUpload.value;
955
+ if (attempt) insertImage(attempt.file);
956
+ }
957
+ }, uploading.value ? "Retrying…" : "Retry")]) : null,
958
+ dragOver.value ? h("div", {
959
+ class: "looma-editor__drop-overlay",
960
+ "aria-hidden": "true"
961
+ }, [loomaIcon("image"), h("span", "Drop image to upload")]) : null,
962
+ h(Popover, {
963
+ class: "looma-editor__table-picker-popover",
964
+ open: tablePickerOpen.value,
965
+ for: tablePickerAnchorId,
966
+ placement: mobile.value ? "top-start" : "bottom-start",
967
+ onClose: () => {
968
+ tablePickerOpen.value = false;
969
+ }
970
+ }, () => [h(EditorInsertTableGrid, {
971
+ open: true,
972
+ onInsert: (detail) => {
973
+ instance?.chain().focus().insertTable(detail).run();
974
+ tablePickerOpen.value = false;
975
+ }
976
+ })]),
977
+ slash.active && slash.items.length > 0 ? h(EditorSlashMenu, {
978
+ open: true,
979
+ query: slash.query,
980
+ items: managedSlashMenuItems(slash.items),
981
+ selectedIndex: slash.selectedIndex,
982
+ anchorRect: managedMenuAnchorRect(slash.rect),
983
+ onHighlight: ({ index }) => {
984
+ slash.selectedIndex = index;
985
+ },
986
+ onSelect: ({ index }) => {
987
+ slash.select?.(index);
988
+ }
989
+ }) : null,
990
+ mention.active && (mention.loading || mention.items.length > 0) ? h(EditorMentionMenu, {
991
+ id: mentionMenuId,
992
+ open: true,
993
+ query: mention.query,
994
+ items: mention.items,
995
+ selectedIndex: mention.selectedIndex,
996
+ anchorRect: managedMenuAnchorRect(mention.rect),
997
+ loading: mention.loading,
998
+ onHighlight: ({ index }) => {
999
+ mention.selectedIndex = index;
1000
+ mention.highlight?.(index);
1001
+ },
1002
+ onSelect: ({ index }) => {
1003
+ mention.select?.(index);
1004
+ }
1005
+ }) : null,
1006
+ tableUi.toolbarOpen ? h("div", {
1007
+ ref: tableToolbarShell,
1008
+ class: "looma-editor__table-toolbar-shell",
1009
+ style: tableUi.toolbarStyle
1010
+ }, [h(EditorTableToolbar, tableProps)]) : null,
1011
+ tableUi.overlayOpen ? h("div", {
1012
+ ref: tableOverlayShell,
1013
+ class: "looma-editor__table-overlay-shell",
1014
+ style: tableUi.overlayStyle,
1015
+ onMousedown: onTableOverlayMouseDown,
1016
+ onPointerleave: onTableOverlayPointerLeave
1017
+ }, [h(EditorTableOverlay, {
1018
+ open: true,
1019
+ geometry: tableUi.geometry,
1020
+ onAction: runOverlayAction
1021
+ })]) : null,
1022
+ tableUi.menuOpen ? h("div", {
1023
+ ref: tableMenuShell,
1024
+ class: "looma-editor__table-menu-shell",
1025
+ style: tableUi.menuStyle
1026
+ }, [h(EditorTableContextMenu, tableProps)]) : null
1027
+ ]);
1028
+ };
1029
+ }
1160
1030
  });
1161
- export {
1162
- EDITOR_ADAPTER_COMPONENT_TAG_MAP,
1163
- EditorInsertTableGrid,
1164
- EditorMentionMenu,
1165
- EditorSlashMenu,
1166
- EditorTableContextMenu,
1167
- EditorTableOverlay,
1168
- EditorTableToolbar,
1169
- EditorToolbar,
1170
- LoomaEditor
1171
- };
1031
+ //#endregion
1032
+ export { EDITOR_ADAPTER_COMPONENT_TAG_MAP, EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, LoomaEditor };