@shelamkoff/rector 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (668) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +17 -0
  3. package/README.md +23 -0
  4. package/THIRD_PARTY_LICENSES.txt +298 -0
  5. package/dist/I18n.d.ts +1 -0
  6. package/dist/core/Block.d.ts +110 -0
  7. package/dist/core/Block.js +390 -0
  8. package/dist/core/BlockAnimator.d.ts +39 -0
  9. package/dist/core/BlockAnimator.js +106 -0
  10. package/dist/core/BlockManager.d.ts +163 -0
  11. package/dist/core/BlockManager.js +640 -0
  12. package/dist/core/BlockOperations.d.ts +67 -0
  13. package/dist/core/BlockOperations.js +238 -0
  14. package/dist/core/ChangeNotifier.d.ts +15 -0
  15. package/dist/core/ChangeNotifier.js +55 -0
  16. package/dist/core/CommandDispatcher.d.ts +52 -0
  17. package/dist/core/CommandDispatcher.js +208 -0
  18. package/dist/core/CrossBlockSelection.d.ts +30 -0
  19. package/dist/core/CrossBlockSelection.js +74 -0
  20. package/dist/core/Diagnostics.d.ts +23 -0
  21. package/dist/core/Diagnostics.js +62 -0
  22. package/dist/core/DocumentSchema.d.ts +29 -0
  23. package/dist/core/DocumentSchema.js +147 -0
  24. package/dist/core/DocumentSnapshotStore.d.ts +34 -0
  25. package/dist/core/DocumentSnapshotStore.js +153 -0
  26. package/dist/core/DragManager.d.ts +17 -0
  27. package/dist/core/DragManager.js +223 -0
  28. package/dist/core/EditorFacade.d.ts +154 -0
  29. package/dist/core/EditorFacade.js +361 -0
  30. package/dist/core/EditorHolderOwnership.d.ts +12 -0
  31. package/dist/core/EditorHolderOwnership.js +28 -0
  32. package/dist/core/I18n.d.ts +147 -0
  33. package/dist/core/I18n.js +296 -0
  34. package/dist/core/InlinePatternMatcher.d.ts +26 -0
  35. package/dist/core/InlinePatternMatcher.js +267 -0
  36. package/dist/core/InlinePluginRegistry.d.ts +55 -0
  37. package/dist/core/InlinePluginRegistry.js +128 -0
  38. package/dist/core/KeyboardManager.d.ts +17 -0
  39. package/dist/core/KeyboardManager.js +231 -0
  40. package/dist/core/LifecycleScope.d.ts +19 -0
  41. package/dist/core/LifecycleScope.js +41 -0
  42. package/dist/core/MouseSelectionManager.d.ts +30 -0
  43. package/dist/core/MouseSelectionManager.js +307 -0
  44. package/dist/core/PluginOwnership.d.ts +9 -0
  45. package/dist/core/PluginOwnership.js +40 -0
  46. package/dist/core/PopupManager.d.ts +50 -0
  47. package/dist/core/PopupManager.js +183 -0
  48. package/dist/core/PreservedBlockPlugin.d.ts +11 -0
  49. package/dist/core/PreservedBlockPlugin.js +42 -0
  50. package/dist/core/PublicEditorApi.d.ts +82 -0
  51. package/dist/core/PublicEditorApi.js +158 -0
  52. package/dist/core/SelectionManager.d.ts +50 -0
  53. package/dist/core/SelectionManager.js +205 -0
  54. package/dist/core/ShortcutRegistry.d.ts +29 -0
  55. package/dist/core/ShortcutRegistry.js +106 -0
  56. package/dist/core/SlashCommands.d.ts +34 -0
  57. package/dist/core/SlashCommands.js +483 -0
  58. package/dist/core/StyleInjector.d.ts +1 -0
  59. package/dist/core/StyleInjector.js +1 -0
  60. package/dist/core/Tooltip.d.ts +17 -0
  61. package/dist/core/Tooltip.js +60 -0
  62. package/dist/core/TriggerManager.d.ts +17 -0
  63. package/dist/core/TriggerManager.js +159 -0
  64. package/dist/core/TypeSelector.d.ts +31 -0
  65. package/dist/core/TypeSelector.js +464 -0
  66. package/dist/core/UndoManager.d.ts +72 -0
  67. package/dist/core/UndoManager.js +383 -0
  68. package/dist/core/block-settings-menu/BlockActions.d.ts +81 -0
  69. package/dist/core/block-settings-menu/BlockActions.js +233 -0
  70. package/dist/core/block-settings-menu/BlockSettingsMenu.d.ts +46 -0
  71. package/dist/core/block-settings-menu/BlockSettingsMenu.js +205 -0
  72. package/dist/core/block-settings-menu/MenuBuilder.d.ts +43 -0
  73. package/dist/core/block-settings-menu/MenuBuilder.js +203 -0
  74. package/dist/core/block-settings-menu/MenuPositioner.d.ts +22 -0
  75. package/dist/core/block-settings-menu/MenuPositioner.js +79 -0
  76. package/dist/core/clipboard/Clipboard.d.ts +47 -0
  77. package/dist/core/clipboard/Clipboard.js +653 -0
  78. package/dist/core/clipboard/CrossBlockEditor.d.ts +43 -0
  79. package/dist/core/clipboard/CrossBlockEditor.js +198 -0
  80. package/dist/core/clipboard/PasteRouter.d.ts +27 -0
  81. package/dist/core/clipboard/PasteRouter.js +86 -0
  82. package/dist/core/clipboard/pasteInsert.d.ts +35 -0
  83. package/dist/core/clipboard/pasteInsert.js +215 -0
  84. package/dist/core/clipboard/pasteUtils.d.ts +25 -0
  85. package/dist/core/clipboard/pasteUtils.js +98 -0
  86. package/dist/core/config.d.ts +77 -0
  87. package/dist/core/config.js +100 -0
  88. package/dist/core/constants.d.ts +23 -0
  89. package/dist/core/constants.js +30 -0
  90. package/dist/core/crossBlockConvert.d.ts +38 -0
  91. package/dist/core/crossBlockConvert.js +253 -0
  92. package/dist/core/dom.d.ts +35 -0
  93. package/dist/core/dom.js +65 -0
  94. package/dist/core/editorEvents.d.ts +89 -0
  95. package/dist/core/editorEvents.js +60 -0
  96. package/dist/core/hydrateInlinePlugins.d.ts +9 -0
  97. package/dist/core/hydrateInlinePlugins.js +36 -0
  98. package/dist/core/icons.d.ts +10 -0
  99. package/dist/core/icons.js +21 -0
  100. package/dist/core/index.d.ts +59 -0
  101. package/dist/core/index.js +703 -0
  102. package/dist/core/inline-toolbar/ActionsPanel.d.ts +61 -0
  103. package/dist/core/inline-toolbar/ActionsPanel.js +132 -0
  104. package/dist/core/inline-toolbar/InlinePositioner.d.ts +20 -0
  105. package/dist/core/inline-toolbar/InlinePositioner.js +63 -0
  106. package/dist/core/inline-toolbar/InlineToolbar.d.ts +45 -0
  107. package/dist/core/inline-toolbar/InlineToolbar.js +456 -0
  108. package/dist/core/inline-toolbar/PluginControlsSlot.d.ts +52 -0
  109. package/dist/core/inline-toolbar/PluginControlsSlot.js +118 -0
  110. package/dist/core/inline-toolbar/SelectionTracker.d.ts +63 -0
  111. package/dist/core/inline-toolbar/SelectionTracker.js +173 -0
  112. package/dist/core/inlinePluginInsert.d.ts +13 -0
  113. package/dist/core/inlinePluginInsert.js +56 -0
  114. package/dist/core/locale/en.d.ts +35 -0
  115. package/dist/core/locale/en.js +37 -0
  116. package/dist/core/locale/ru.d.ts +35 -0
  117. package/dist/core/locale/ru.js +37 -0
  118. package/dist/core/menuKeyboardNav.d.ts +15 -0
  119. package/dist/core/menuKeyboardNav.js +52 -0
  120. package/dist/core/sanitize.d.ts +2 -0
  121. package/dist/core/sanitize.js +6 -0
  122. package/dist/core/splitConvert.d.ts +46 -0
  123. package/dist/core/splitConvert.js +190 -0
  124. package/dist/core/textOffset.d.ts +46 -0
  125. package/dist/core/textOffset.js +180 -0
  126. package/dist/core/themes/dark.css +30 -0
  127. package/dist/core/themes/light.css +33 -0
  128. package/dist/core/themes/variables.css +1170 -0
  129. package/dist/core/toolbar/OffcanvasRoot.d.ts +37 -0
  130. package/dist/core/toolbar/OffcanvasRoot.js +127 -0
  131. package/dist/core/toolbar/Toolbar.d.ts +70 -0
  132. package/dist/core/toolbar/Toolbar.js +432 -0
  133. package/dist/core/toolbar/ToolbarPositioner.d.ts +60 -0
  134. package/dist/core/toolbar/ToolbarPositioner.js +188 -0
  135. package/dist/core/toolbar/ToolboxBuilder.d.ts +44 -0
  136. package/dist/core/toolbar/ToolboxBuilder.js +260 -0
  137. package/dist/core/types.d.ts +747 -0
  138. package/dist/core/uid.d.ts +6 -0
  139. package/dist/core/uid.js +29 -0
  140. package/dist/index.d.ts +2 -0
  141. package/dist/index.js +1 -0
  142. package/dist/inline-plugins/README.md +35 -0
  143. package/dist/inline-plugins/README.ru.md +35 -0
  144. package/dist/inline-plugins/color/README.md +61 -0
  145. package/dist/inline-plugins/color/README.ru.md +61 -0
  146. package/dist/inline-plugins/color.d.ts +8 -0
  147. package/dist/inline-plugins/color.js +184 -0
  148. package/dist/inline-plugins/locale/en.d.ts +7 -0
  149. package/dist/inline-plugins/locale/en.js +6 -0
  150. package/dist/inline-plugins/locale/ru.d.ts +7 -0
  151. package/dist/inline-plugins/locale/ru.js +6 -0
  152. package/dist/inline-plugins/mention/README.md +97 -0
  153. package/dist/inline-plugins/mention/README.ru.md +97 -0
  154. package/dist/inline-plugins/mention/index.d.ts +110 -0
  155. package/dist/inline-plugins/mention/index.js +1897 -0
  156. package/dist/inline-plugins/mention/styles.css +183 -0
  157. package/dist/inline-plugins/mention/widget.d.ts +16 -0
  158. package/dist/inline-plugins/mention/widget.js +75 -0
  159. package/dist/inline-tools/align.d.ts +14 -0
  160. package/dist/inline-tools/align.js +120 -0
  161. package/dist/inline-tools/bold.d.ts +8 -0
  162. package/dist/inline-tools/bold.js +12 -0
  163. package/dist/inline-tools/caseTransform.d.ts +8 -0
  164. package/dist/inline-tools/caseTransform.js +87 -0
  165. package/dist/inline-tools/clearFormatting.d.ts +8 -0
  166. package/dist/inline-tools/clearFormatting.js +74 -0
  167. package/dist/inline-tools/code.d.ts +8 -0
  168. package/dist/inline-tools/code.js +12 -0
  169. package/dist/inline-tools/colorPicker.d.ts +7 -0
  170. package/dist/inline-tools/colorPicker.js +271 -0
  171. package/dist/inline-tools/defaults.d.ts +10 -0
  172. package/dist/inline-tools/defaults.js +57 -0
  173. package/dist/inline-tools/fontSize.d.ts +7 -0
  174. package/dist/inline-tools/fontSize.js +524 -0
  175. package/dist/inline-tools/italic.d.ts +8 -0
  176. package/dist/inline-tools/italic.js +12 -0
  177. package/dist/inline-tools/link.d.ts +12 -0
  178. package/dist/inline-tools/link.js +251 -0
  179. package/dist/inline-tools/marker.d.ts +8 -0
  180. package/dist/inline-tools/marker.js +12 -0
  181. package/dist/inline-tools/scriptTool.d.ts +11 -0
  182. package/dist/inline-tools/scriptTool.js +149 -0
  183. package/dist/inline-tools/strikethrough.d.ts +8 -0
  184. package/dist/inline-tools/strikethrough.js +12 -0
  185. package/dist/inline-tools/utils.d.ts +220 -0
  186. package/dist/inline-tools/utils.js +992 -0
  187. package/dist/locale/en.d.ts +311 -0
  188. package/dist/locale/en.js +52 -0
  189. package/dist/locale/ru.d.ts +315 -0
  190. package/dist/locale/ru.js +52 -0
  191. package/dist/plugins/BlockPluginAbstract.d.ts +82 -0
  192. package/dist/plugins/BlockPluginAbstract.js +111 -0
  193. package/dist/plugins/README.md +44 -0
  194. package/dist/plugins/README.ru.md +40 -0
  195. package/dist/plugins/async.d.ts +35 -0
  196. package/dist/plugins/async.js +79 -0
  197. package/dist/plugins/attaches/README.md +86 -0
  198. package/dist/plugins/attaches/README.ru.md +87 -0
  199. package/dist/plugins/attaches/attaches.css +374 -0
  200. package/dist/plugins/attaches/index.d.ts +133 -0
  201. package/dist/plugins/attaches/index.js +921 -0
  202. package/dist/plugins/attaches/locale/en.d.ts +23 -0
  203. package/dist/plugins/attaches/locale/en.js +19 -0
  204. package/dist/plugins/attaches/locale/keys.d.ts +5 -0
  205. package/dist/plugins/attaches/locale/ru.d.ts +25 -0
  206. package/dist/plugins/attaches/locale/ru.js +19 -0
  207. package/dist/plugins/carousel/README.md +108 -0
  208. package/dist/plugins/carousel/README.ru.md +109 -0
  209. package/dist/plugins/carousel/carousel.css +198 -0
  210. package/dist/plugins/carousel/icons.d.ts +12 -0
  211. package/dist/plugins/carousel/icons.js +13 -0
  212. package/dist/plugins/carousel/index.d.ts +137 -0
  213. package/dist/plugins/carousel/index.js +859 -0
  214. package/dist/plugins/carousel/locale/en.d.ts +41 -0
  215. package/dist/plugins/carousel/locale/en.js +40 -0
  216. package/dist/plugins/carousel/locale/keys.d.ts +2 -0
  217. package/dist/plugins/carousel/locale/ru.d.ts +41 -0
  218. package/dist/plugins/carousel/locale/ru.js +40 -0
  219. package/dist/plugins/carousel/state.d.ts +29 -0
  220. package/dist/plugins/carousel/state.js +42 -0
  221. package/dist/plugins/checklist/README.md +54 -0
  222. package/dist/plugins/checklist/README.ru.md +55 -0
  223. package/dist/plugins/checklist/checklist.css +80 -0
  224. package/dist/plugins/checklist/index.d.ts +75 -0
  225. package/dist/plugins/checklist/index.js +407 -0
  226. package/dist/plugins/checklist/locale/en.d.ts +5 -0
  227. package/dist/plugins/checklist/locale/en.js +4 -0
  228. package/dist/plugins/checklist/locale/keys.d.ts +5 -0
  229. package/dist/plugins/checklist/locale/ru.d.ts +5 -0
  230. package/dist/plugins/checklist/locale/ru.js +4 -0
  231. package/dist/plugins/checklist/mapTextFields.d.ts +1 -0
  232. package/dist/plugins/checklist/mapTextFields.js +1 -0
  233. package/dist/plugins/code/README.md +59 -0
  234. package/dist/plugins/code/README.ru.md +60 -0
  235. package/dist/plugins/code/code.css +360 -0
  236. package/dist/plugins/code/index.d.ts +109 -0
  237. package/dist/plugins/code/index.js +1012 -0
  238. package/dist/plugins/code/locale/en.d.ts +11 -0
  239. package/dist/plugins/code/locale/en.js +10 -0
  240. package/dist/plugins/code/locale/keys.d.ts +5 -0
  241. package/dist/plugins/code/locale/ru.d.ts +11 -0
  242. package/dist/plugins/code/locale/ru.js +10 -0
  243. package/dist/plugins/columns/README.md +55 -0
  244. package/dist/plugins/columns/README.ru.md +56 -0
  245. package/dist/plugins/columns/columns.css +103 -0
  246. package/dist/plugins/columns/index.d.ts +58 -0
  247. package/dist/plugins/columns/index.js +236 -0
  248. package/dist/plugins/columns/locale/en.d.ts +6 -0
  249. package/dist/plugins/columns/locale/en.js +5 -0
  250. package/dist/plugins/columns/locale/keys.d.ts +5 -0
  251. package/dist/plugins/columns/locale/ru.d.ts +6 -0
  252. package/dist/plugins/columns/locale/ru.js +5 -0
  253. package/dist/plugins/delimiter/README.md +48 -0
  254. package/dist/plugins/delimiter/README.ru.md +49 -0
  255. package/dist/plugins/delimiter/delimiter.css +22 -0
  256. package/dist/plugins/delimiter/index.d.ts +38 -0
  257. package/dist/plugins/delimiter/index.js +69 -0
  258. package/dist/plugins/delimiter/locale/en.d.ts +4 -0
  259. package/dist/plugins/delimiter/locale/en.js +3 -0
  260. package/dist/plugins/delimiter/locale/keys.d.ts +5 -0
  261. package/dist/plugins/delimiter/locale/ru.d.ts +4 -0
  262. package/dist/plugins/delimiter/locale/ru.js +3 -0
  263. package/dist/plugins/embed/README.md +81 -0
  264. package/dist/plugins/embed/README.ru.md +82 -0
  265. package/dist/plugins/embed/embed.css +558 -0
  266. package/dist/plugins/embed/index.d.ts +271 -0
  267. package/dist/plugins/embed/index.js +967 -0
  268. package/dist/plugins/embed/locale/en.d.ts +20 -0
  269. package/dist/plugins/embed/locale/en.js +19 -0
  270. package/dist/plugins/embed/locale/keys.d.ts +5 -0
  271. package/dist/plugins/embed/locale/ru.d.ts +20 -0
  272. package/dist/plugins/embed/locale/ru.js +19 -0
  273. package/dist/plugins/embed/player.d.ts +1 -0
  274. package/dist/plugins/embed/player.js +1 -0
  275. package/dist/plugins/gallery/README.md +92 -0
  276. package/dist/plugins/gallery/README.ru.md +93 -0
  277. package/dist/plugins/gallery/css.d.ts +52 -0
  278. package/dist/plugins/gallery/css.js +67 -0
  279. package/dist/plugins/gallery/gallery.css +860 -0
  280. package/dist/plugins/gallery/icons.d.ts +13 -0
  281. package/dist/plugins/gallery/icons.js +45 -0
  282. package/dist/plugins/gallery/index.d.ts +126 -0
  283. package/dist/plugins/gallery/index.js +355 -0
  284. package/dist/plugins/gallery/layout.d.ts +35 -0
  285. package/dist/plugins/gallery/layout.js +79 -0
  286. package/dist/plugins/gallery/locale/en.d.ts +36 -0
  287. package/dist/plugins/gallery/locale/en.js +35 -0
  288. package/dist/plugins/gallery/locale/keys.d.ts +5 -0
  289. package/dist/plugins/gallery/locale/ru.d.ts +36 -0
  290. package/dist/plugins/gallery/locale/ru.js +35 -0
  291. package/dist/plugins/gallery/settings.d.ts +25 -0
  292. package/dist/plugins/gallery/settings.js +199 -0
  293. package/dist/plugins/gallery/slot.d.ts +73 -0
  294. package/dist/plugins/gallery/slot.js +260 -0
  295. package/dist/plugins/gallery/state.d.ts +67 -0
  296. package/dist/plugins/gallery/state.js +133 -0
  297. package/dist/plugins/gallery/styles.d.ts +10 -0
  298. package/dist/plugins/gallery/styles.js +24 -0
  299. package/dist/plugins/gallery/uploader.d.ts +31 -0
  300. package/dist/plugins/gallery/uploader.js +111 -0
  301. package/dist/plugins/gallery/view-empty.d.ts +41 -0
  302. package/dist/plugins/gallery/view-empty.js +56 -0
  303. package/dist/plugins/gallery/view-filled.d.ts +55 -0
  304. package/dist/plugins/gallery/view-filled.js +353 -0
  305. package/dist/plugins/heading/HeadingLevelSelect.d.ts +16 -0
  306. package/dist/plugins/heading/HeadingLevelSelect.js +183 -0
  307. package/dist/plugins/heading/README.md +58 -0
  308. package/dist/plugins/heading/README.ru.md +59 -0
  309. package/dist/plugins/heading/heading.css +76 -0
  310. package/dist/plugins/heading/index.d.ts +124 -0
  311. package/dist/plugins/heading/index.js +273 -0
  312. package/dist/plugins/heading/locale/en.d.ts +11 -0
  313. package/dist/plugins/heading/locale/en.js +10 -0
  314. package/dist/plugins/heading/locale/keys.d.ts +5 -0
  315. package/dist/plugins/heading/locale/ru.d.ts +11 -0
  316. package/dist/plugins/heading/locale/ru.js +10 -0
  317. package/dist/plugins/heading/mapTextFields.d.ts +1 -0
  318. package/dist/plugins/heading/mapTextFields.js +1 -0
  319. package/dist/plugins/image/README.md +93 -0
  320. package/dist/plugins/image/README.ru.md +94 -0
  321. package/dist/plugins/image/css.d.ts +47 -0
  322. package/dist/plugins/image/css.js +47 -0
  323. package/dist/plugins/image/icons.d.ts +13 -0
  324. package/dist/plugins/image/icons.js +26 -0
  325. package/dist/plugins/image/image.css +559 -0
  326. package/dist/plugins/image/index.d.ts +129 -0
  327. package/dist/plugins/image/index.js +346 -0
  328. package/dist/plugins/image/locale/en.d.ts +39 -0
  329. package/dist/plugins/image/locale/en.js +38 -0
  330. package/dist/plugins/image/locale/keys.d.ts +5 -0
  331. package/dist/plugins/image/locale/ru.d.ts +39 -0
  332. package/dist/plugins/image/locale/ru.js +38 -0
  333. package/dist/plugins/image/settings.d.ts +23 -0
  334. package/dist/plugins/image/settings.js +347 -0
  335. package/dist/plugins/image/state.d.ts +86 -0
  336. package/dist/plugins/image/state.js +151 -0
  337. package/dist/plugins/image/styles.d.ts +21 -0
  338. package/dist/plugins/image/styles.js +58 -0
  339. package/dist/plugins/image/uploader.d.ts +31 -0
  340. package/dist/plugins/image/uploader.js +92 -0
  341. package/dist/plugins/image/view-empty.d.ts +44 -0
  342. package/dist/plugins/image/view-empty.js +61 -0
  343. package/dist/plugins/image/view-filled.d.ts +49 -0
  344. package/dist/plugins/image/view-filled.js +282 -0
  345. package/dist/plugins/index.d.ts +21 -0
  346. package/dist/plugins/index.js +22 -0
  347. package/dist/plugins/link-preview/README.md +69 -0
  348. package/dist/plugins/link-preview/README.ru.md +70 -0
  349. package/dist/plugins/link-preview/index.d.ts +169 -0
  350. package/dist/plugins/link-preview/index.js +698 -0
  351. package/dist/plugins/link-preview/link-preview.css +254 -0
  352. package/dist/plugins/link-preview/locale/en.d.ts +14 -0
  353. package/dist/plugins/link-preview/locale/en.js +13 -0
  354. package/dist/plugins/link-preview/locale/keys.d.ts +5 -0
  355. package/dist/plugins/link-preview/locale/ru.d.ts +14 -0
  356. package/dist/plugins/link-preview/locale/ru.js +13 -0
  357. package/dist/plugins/list/README.md +59 -0
  358. package/dist/plugins/list/README.ru.md +60 -0
  359. package/dist/plugins/list/index.d.ts +134 -0
  360. package/dist/plugins/list/index.js +566 -0
  361. package/dist/plugins/list/list.css +57 -0
  362. package/dist/plugins/list/locale/en.d.ts +5 -0
  363. package/dist/plugins/list/locale/en.js +4 -0
  364. package/dist/plugins/list/locale/keys.d.ts +5 -0
  365. package/dist/plugins/list/locale/ru.d.ts +5 -0
  366. package/dist/plugins/list/locale/ru.js +4 -0
  367. package/dist/plugins/list/mapTextFields.d.ts +1 -0
  368. package/dist/plugins/list/mapTextFields.js +1 -0
  369. package/dist/plugins/paragraph/README.md +57 -0
  370. package/dist/plugins/paragraph/README.ru.md +58 -0
  371. package/dist/plugins/paragraph/index.d.ts +112 -0
  372. package/dist/plugins/paragraph/index.js +158 -0
  373. package/dist/plugins/paragraph/locale/en.d.ts +5 -0
  374. package/dist/plugins/paragraph/locale/en.js +4 -0
  375. package/dist/plugins/paragraph/locale/keys.d.ts +5 -0
  376. package/dist/plugins/paragraph/locale/ru.d.ts +5 -0
  377. package/dist/plugins/paragraph/locale/ru.js +4 -0
  378. package/dist/plugins/paragraph/mapTextFields.d.ts +1 -0
  379. package/dist/plugins/paragraph/mapTextFields.js +1 -0
  380. package/dist/plugins/paragraph/paragraph.css +56 -0
  381. package/dist/plugins/person/README.md +72 -0
  382. package/dist/plugins/person/README.ru.md +73 -0
  383. package/dist/plugins/person/index.d.ts +189 -0
  384. package/dist/plugins/person/index.js +777 -0
  385. package/dist/plugins/person/locale/en.d.ts +18 -0
  386. package/dist/plugins/person/locale/en.js +17 -0
  387. package/dist/plugins/person/locale/keys.d.ts +5 -0
  388. package/dist/plugins/person/locale/ru.d.ts +18 -0
  389. package/dist/plugins/person/locale/ru.js +17 -0
  390. package/dist/plugins/person/person.css +431 -0
  391. package/dist/plugins/person/socialResolver.d.ts +18 -0
  392. package/dist/plugins/person/socialResolver.js +69 -0
  393. package/dist/plugins/poll/README.md +96 -0
  394. package/dist/plugins/poll/README.ru.md +97 -0
  395. package/dist/plugins/poll/index.d.ts +134 -0
  396. package/dist/plugins/poll/index.js +825 -0
  397. package/dist/plugins/poll/locale/en.d.ts +23 -0
  398. package/dist/plugins/poll/locale/en.js +22 -0
  399. package/dist/plugins/poll/locale/keys.d.ts +5 -0
  400. package/dist/plugins/poll/locale/ru.d.ts +23 -0
  401. package/dist/plugins/poll/locale/ru.js +22 -0
  402. package/dist/plugins/poll/poll.css +400 -0
  403. package/dist/plugins/quote/README.md +55 -0
  404. package/dist/plugins/quote/README.ru.md +56 -0
  405. package/dist/plugins/quote/index.d.ts +81 -0
  406. package/dist/plugins/quote/index.js +165 -0
  407. package/dist/plugins/quote/locale/en.d.ts +6 -0
  408. package/dist/plugins/quote/locale/en.js +5 -0
  409. package/dist/plugins/quote/locale/keys.d.ts +5 -0
  410. package/dist/plugins/quote/locale/ru.d.ts +6 -0
  411. package/dist/plugins/quote/locale/ru.js +5 -0
  412. package/dist/plugins/quote/mapTextFields.d.ts +1 -0
  413. package/dist/plugins/quote/mapTextFields.js +1 -0
  414. package/dist/plugins/quote/quote.css +72 -0
  415. package/dist/plugins/raw/README.md +54 -0
  416. package/dist/plugins/raw/README.ru.md +55 -0
  417. package/dist/plugins/raw/index.d.ts +62 -0
  418. package/dist/plugins/raw/index.js +227 -0
  419. package/dist/plugins/raw/locale/en.d.ts +7 -0
  420. package/dist/plugins/raw/locale/en.js +6 -0
  421. package/dist/plugins/raw/locale/keys.d.ts +5 -0
  422. package/dist/plugins/raw/locale/ru.d.ts +7 -0
  423. package/dist/plugins/raw/locale/ru.js +6 -0
  424. package/dist/plugins/raw/raw.css +127 -0
  425. package/dist/plugins/shared/actionBar.d.ts +19 -0
  426. package/dist/plugins/shared/actionBar.js +35 -0
  427. package/dist/plugins/shared/dropzone.d.ts +106 -0
  428. package/dist/plugins/shared/dropzone.js +107 -0
  429. package/dist/plugins/shared/fileInput.d.ts +44 -0
  430. package/dist/plugins/shared/fileInput.js +62 -0
  431. package/dist/plugins/spoiler/README.md +55 -0
  432. package/dist/plugins/spoiler/README.ru.md +56 -0
  433. package/dist/plugins/spoiler/index.d.ts +61 -0
  434. package/dist/plugins/spoiler/index.js +169 -0
  435. package/dist/plugins/spoiler/locale/en.d.ts +7 -0
  436. package/dist/plugins/spoiler/locale/en.js +6 -0
  437. package/dist/plugins/spoiler/locale/keys.d.ts +5 -0
  438. package/dist/plugins/spoiler/locale/ru.d.ts +7 -0
  439. package/dist/plugins/spoiler/locale/ru.js +6 -0
  440. package/dist/plugins/spoiler/spoiler.css +101 -0
  441. package/dist/plugins/table/README.md +55 -0
  442. package/dist/plugins/table/README.ru.md +56 -0
  443. package/dist/plugins/table/index.d.ts +85 -0
  444. package/dist/plugins/table/index.js +399 -0
  445. package/dist/plugins/table/locale/en.d.ts +9 -0
  446. package/dist/plugins/table/locale/en.js +8 -0
  447. package/dist/plugins/table/locale/keys.d.ts +5 -0
  448. package/dist/plugins/table/locale/ru.d.ts +9 -0
  449. package/dist/plugins/table/locale/ru.js +8 -0
  450. package/dist/plugins/table/table.css +58 -0
  451. package/dist/plugins/toggle/README.md +56 -0
  452. package/dist/plugins/toggle/README.ru.md +57 -0
  453. package/dist/plugins/toggle/index.d.ts +69 -0
  454. package/dist/plugins/toggle/index.js +191 -0
  455. package/dist/plugins/toggle/locale/en.d.ts +7 -0
  456. package/dist/plugins/toggle/locale/en.js +6 -0
  457. package/dist/plugins/toggle/locale/keys.d.ts +5 -0
  458. package/dist/plugins/toggle/locale/ru.d.ts +7 -0
  459. package/dist/plugins/toggle/locale/ru.js +6 -0
  460. package/dist/plugins/toggle/toggle.css +101 -0
  461. package/dist/plugins/warning/README.md +55 -0
  462. package/dist/plugins/warning/README.ru.md +56 -0
  463. package/dist/plugins/warning/index.d.ts +66 -0
  464. package/dist/plugins/warning/index.js +167 -0
  465. package/dist/plugins/warning/locale/en.d.ts +6 -0
  466. package/dist/plugins/warning/locale/en.js +5 -0
  467. package/dist/plugins/warning/locale/keys.d.ts +5 -0
  468. package/dist/plugins/warning/locale/ru.d.ts +6 -0
  469. package/dist/plugins/warning/locale/ru.js +5 -0
  470. package/dist/plugins/warning/warning.css +63 -0
  471. package/dist/renderer/async.d.ts +1 -0
  472. package/dist/renderer/async.js +7 -0
  473. package/dist/renderer/errors.d.ts +27 -0
  474. package/dist/renderer/errors.js +48 -0
  475. package/dist/renderer/index.d.ts +82 -0
  476. package/dist/renderer/index.js +413 -0
  477. package/dist/renderer/inline.d.ts +5 -0
  478. package/dist/renderer/inline.js +61 -0
  479. package/dist/renderer/locale/en.d.ts +35 -0
  480. package/dist/renderer/locale/en.js +29 -0
  481. package/dist/renderer/locale/ru.d.ts +37 -0
  482. package/dist/renderer/locale/ru.js +29 -0
  483. package/dist/renderer/renderers/README.md +7 -0
  484. package/dist/renderer/renderers/README.ru.md +5 -0
  485. package/dist/renderer/renderers/async.d.ts +51 -0
  486. package/dist/renderer/renderers/async.js +99 -0
  487. package/dist/renderer/renderers/attaches/README.md +48 -0
  488. package/dist/renderer/renderers/attaches/README.ru.md +48 -0
  489. package/dist/renderer/renderers/attaches/index.d.ts +37 -0
  490. package/dist/renderer/renderers/attaches/index.js +451 -0
  491. package/dist/renderer/renderers/attaches/locale/en.d.ts +11 -0
  492. package/dist/renderer/renderers/attaches/locale/en.js +7 -0
  493. package/dist/renderer/renderers/attaches/locale/ru.d.ts +13 -0
  494. package/dist/renderer/renderers/attaches/locale/ru.js +7 -0
  495. package/dist/renderer/renderers/attaches/styles.css +196 -0
  496. package/dist/renderer/renderers/carousel/README.md +43 -0
  497. package/dist/renderer/renderers/carousel/README.ru.md +43 -0
  498. package/dist/renderer/renderers/carousel/index.d.ts +7 -0
  499. package/dist/renderer/renderers/carousel/index.js +110 -0
  500. package/dist/renderer/renderers/carousel/styles.css +7 -0
  501. package/dist/renderer/renderers/checklist/README.md +33 -0
  502. package/dist/renderer/renderers/checklist/README.ru.md +33 -0
  503. package/dist/renderer/renderers/checklist/index.d.ts +7 -0
  504. package/dist/renderer/renderers/checklist/index.js +55 -0
  505. package/dist/renderer/renderers/checklist/styles.css +54 -0
  506. package/dist/renderer/renderers/code/README.md +33 -0
  507. package/dist/renderer/renderers/code/README.ru.md +33 -0
  508. package/dist/renderer/renderers/code/index.d.ts +9 -0
  509. package/dist/renderer/renderers/code/index.js +137 -0
  510. package/dist/renderer/renderers/code/locale/en.d.ts +5 -0
  511. package/dist/renderer/renderers/code/locale/en.js +4 -0
  512. package/dist/renderer/renderers/code/locale/ru.d.ts +5 -0
  513. package/dist/renderer/renderers/code/locale/ru.js +4 -0
  514. package/dist/renderer/renderers/code/styles.css +330 -0
  515. package/dist/renderer/renderers/columns/README.md +33 -0
  516. package/dist/renderer/renderers/columns/README.ru.md +33 -0
  517. package/dist/renderer/renderers/columns/index.d.ts +7 -0
  518. package/dist/renderer/renderers/columns/index.js +53 -0
  519. package/dist/renderer/renderers/columns/styles.css +5 -0
  520. package/dist/renderer/renderers/delimiter/README.md +33 -0
  521. package/dist/renderer/renderers/delimiter/README.ru.md +33 -0
  522. package/dist/renderer/renderers/delimiter/index.d.ts +7 -0
  523. package/dist/renderer/renderers/delimiter/index.js +28 -0
  524. package/dist/renderer/renderers/delimiter/styles.css +12 -0
  525. package/dist/renderer/renderers/embed/README.md +35 -0
  526. package/dist/renderer/renderers/embed/README.ru.md +35 -0
  527. package/dist/renderer/renderers/embed/index.d.ts +8 -0
  528. package/dist/renderer/renderers/embed/index.js +66 -0
  529. package/dist/renderer/renderers/embed/styles.css +217 -0
  530. package/dist/renderer/renderers/gallery/README.md +41 -0
  531. package/dist/renderer/renderers/gallery/README.ru.md +41 -0
  532. package/dist/renderer/renderers/gallery/index.d.ts +8 -0
  533. package/dist/renderer/renderers/gallery/index.js +329 -0
  534. package/dist/renderer/renderers/gallery/styles.css +419 -0
  535. package/dist/renderer/renderers/heading/README.md +33 -0
  536. package/dist/renderer/renderers/heading/README.ru.md +33 -0
  537. package/dist/renderer/renderers/heading/index.d.ts +8 -0
  538. package/dist/renderer/renderers/heading/index.js +38 -0
  539. package/dist/renderer/renderers/heading/styles.css +26 -0
  540. package/dist/renderer/renderers/image/README.md +42 -0
  541. package/dist/renderer/renderers/image/README.ru.md +42 -0
  542. package/dist/renderer/renderers/image/index.d.ts +7 -0
  543. package/dist/renderer/renderers/image/index.js +93 -0
  544. package/dist/renderer/renderers/image/styles.css +40 -0
  545. package/dist/renderer/renderers/index.d.ts +47 -0
  546. package/dist/renderer/renderers/index.js +125 -0
  547. package/dist/renderer/renderers/link-preview/README.md +43 -0
  548. package/dist/renderer/renderers/link-preview/README.ru.md +43 -0
  549. package/dist/renderer/renderers/link-preview/index.d.ts +9 -0
  550. package/dist/renderer/renderers/link-preview/index.js +118 -0
  551. package/dist/renderer/renderers/link-preview/styles.css +246 -0
  552. package/dist/renderer/renderers/list/README.md +33 -0
  553. package/dist/renderer/renderers/list/README.ru.md +33 -0
  554. package/dist/renderer/renderers/list/index.d.ts +8 -0
  555. package/dist/renderer/renderers/list/index.js +42 -0
  556. package/dist/renderer/renderers/list/styles.css +20 -0
  557. package/dist/renderer/renderers/paragraph/README.md +33 -0
  558. package/dist/renderer/renderers/paragraph/README.ru.md +33 -0
  559. package/dist/renderer/renderers/paragraph/index.d.ts +7 -0
  560. package/dist/renderer/renderers/paragraph/index.js +35 -0
  561. package/dist/renderer/renderers/paragraph/styles.css +3 -0
  562. package/dist/renderer/renderers/person/README.md +44 -0
  563. package/dist/renderer/renderers/person/README.ru.md +44 -0
  564. package/dist/renderer/renderers/person/index.d.ts +7 -0
  565. package/dist/renderer/renderers/person/index.js +213 -0
  566. package/dist/renderer/renderers/person/locale/en.d.ts +6 -0
  567. package/dist/renderer/renderers/person/locale/en.js +5 -0
  568. package/dist/renderer/renderers/person/locale/ru.d.ts +6 -0
  569. package/dist/renderer/renderers/person/locale/ru.js +5 -0
  570. package/dist/renderer/renderers/person/styles.css +47 -0
  571. package/dist/renderer/renderers/poll/README.md +68 -0
  572. package/dist/renderer/renderers/poll/README.ru.md +68 -0
  573. package/dist/renderer/renderers/poll/index.d.ts +9 -0
  574. package/dist/renderer/renderers/poll/index.js +339 -0
  575. package/dist/renderer/renderers/poll/styles.css +148 -0
  576. package/dist/renderer/renderers/quote/README.md +33 -0
  577. package/dist/renderer/renderers/quote/README.ru.md +33 -0
  578. package/dist/renderer/renderers/quote/index.d.ts +7 -0
  579. package/dist/renderer/renderers/quote/index.js +59 -0
  580. package/dist/renderer/renderers/quote/styles.css +43 -0
  581. package/dist/renderer/renderers/raw/README.md +33 -0
  582. package/dist/renderer/renderers/raw/README.ru.md +33 -0
  583. package/dist/renderer/renderers/raw/index.d.ts +7 -0
  584. package/dist/renderer/renderers/raw/index.js +27 -0
  585. package/dist/renderer/renderers/spoiler/README.md +33 -0
  586. package/dist/renderer/renderers/spoiler/README.ru.md +33 -0
  587. package/dist/renderer/renderers/spoiler/index.d.ts +9 -0
  588. package/dist/renderer/renderers/spoiler/index.js +84 -0
  589. package/dist/renderer/renderers/spoiler/locale/en.d.ts +4 -0
  590. package/dist/renderer/renderers/spoiler/locale/en.js +3 -0
  591. package/dist/renderer/renderers/spoiler/locale/ru.d.ts +4 -0
  592. package/dist/renderer/renderers/spoiler/locale/ru.js +3 -0
  593. package/dist/renderer/renderers/spoiler/styles.css +61 -0
  594. package/dist/renderer/renderers/table/README.md +33 -0
  595. package/dist/renderer/renderers/table/README.ru.md +33 -0
  596. package/dist/renderer/renderers/table/index.d.ts +7 -0
  597. package/dist/renderer/renderers/table/index.js +83 -0
  598. package/dist/renderer/renderers/table/styles.css +34 -0
  599. package/dist/renderer/renderers/toggle/README.md +33 -0
  600. package/dist/renderer/renderers/toggle/README.ru.md +33 -0
  601. package/dist/renderer/renderers/toggle/index.d.ts +7 -0
  602. package/dist/renderer/renderers/toggle/index.js +59 -0
  603. package/dist/renderer/renderers/toggle/styles.css +49 -0
  604. package/dist/renderer/renderers/warning/README.md +33 -0
  605. package/dist/renderer/renderers/warning/README.ru.md +33 -0
  606. package/dist/renderer/renderers/warning/index.d.ts +7 -0
  607. package/dist/renderer/renderers/warning/index.js +62 -0
  608. package/dist/renderer/renderers/warning/styles.css +41 -0
  609. package/dist/renderer/styles/base.css +168 -0
  610. package/dist/renderer/types.d.ts +464 -0
  611. package/dist/shared/blockDataNormalizers.d.ts +14 -0
  612. package/dist/shared/blockDataNormalizers.js +235 -0
  613. package/dist/shared/blockDataValidators.d.ts +81 -0
  614. package/dist/shared/blockDataValidators.js +313 -0
  615. package/dist/shared/blockTypes.d.ts +2 -0
  616. package/dist/shared/blockTypes.js +26 -0
  617. package/dist/shared/carouselData.d.ts +34 -0
  618. package/dist/shared/carouselData.js +111 -0
  619. package/dist/shared/cloneEditorData.d.ts +11 -0
  620. package/dist/shared/cloneEditorData.js +20 -0
  621. package/dist/shared/documentTypes.d.ts +35 -0
  622. package/dist/shared/embedPlayer.d.ts +42 -0
  623. package/dist/shared/embedPlayer.js +141 -0
  624. package/dist/shared/fileUtils.d.ts +33 -0
  625. package/dist/shared/fileUtils.js +109 -0
  626. package/dist/shared/highlightRuntime.d.ts +38 -0
  627. package/dist/shared/highlightRuntime.js +75 -0
  628. package/dist/shared/inlineMarshal.d.ts +82 -0
  629. package/dist/shared/inlineMarshal.js +215 -0
  630. package/dist/shared/jsonData.d.ts +11 -0
  631. package/dist/shared/jsonData.js +53 -0
  632. package/dist/shared/localeTypes.d.ts +11 -0
  633. package/dist/shared/mapTextFields.d.ts +89 -0
  634. package/dist/shared/mapTextFields.js +119 -0
  635. package/dist/shared/pollData.d.ts +90 -0
  636. package/dist/shared/pollData.js +231 -0
  637. package/dist/shared/resolvePath.d.ts +8 -0
  638. package/dist/shared/resolvePath.js +10 -0
  639. package/dist/shared/runtime/dompurify.js +2424 -0
  640. package/dist/shared/runtime/highlightBundle.js +10 -0
  641. package/dist/shared/runtime/jszip.js +13 -0
  642. package/dist/shared/sanitize/allowlist.d.ts +21 -0
  643. package/dist/shared/sanitize/allowlist.js +40 -0
  644. package/dist/shared/sanitize/escapeHtml.d.ts +6 -0
  645. package/dist/shared/sanitize/escapeHtml.js +11 -0
  646. package/dist/shared/sanitize/index.d.ts +8 -0
  647. package/dist/shared/sanitize/index.js +8 -0
  648. package/dist/shared/sanitize/parseInline.d.ts +11 -0
  649. package/dist/shared/sanitize/parseInline.js +25 -0
  650. package/dist/shared/sanitize/sanitizeHtml.d.ts +8 -0
  651. package/dist/shared/sanitize/sanitizeHtml.js +19 -0
  652. package/dist/shared/sanitize/sanitizeRawHtml.d.ts +15 -0
  653. package/dist/shared/sanitize/sanitizeRawHtml.js +162 -0
  654. package/dist/shared/sanitize/sanitizeStyle.d.ts +9 -0
  655. package/dist/shared/sanitize/sanitizeStyle.js +39 -0
  656. package/dist/shared/sanitize/sanitizeUrl.d.ts +28 -0
  657. package/dist/shared/sanitize/sanitizeUrl.js +86 -0
  658. package/dist/shared/sanitize/walker.d.ts +15 -0
  659. package/dist/shared/sanitize/walker.js +98 -0
  660. package/dist/shared/styleRegistry.d.ts +4 -0
  661. package/dist/shared/styleRegistry.js +38 -0
  662. package/dist/shared/textFormat.d.ts +32 -0
  663. package/dist/shared/textFormat.js +48 -0
  664. package/dist/shared/zipRuntime.d.ts +19 -0
  665. package/dist/shared/zipRuntime.js +54 -0
  666. package/dist/styles/editor.css +3 -0
  667. package/dist/types.d.ts +1 -0
  668. package/package.json +149 -0
@@ -0,0 +1,1897 @@
1
+ // @ts-check
2
+ /**
3
+ * Mention inline plugin.
4
+ *
5
+ * A port of `@shelamkoff/mentionjs` adapted to Rector's InlinePlugin
6
+ * contract. It preserves the customization surface (searchFunction,
7
+ * debounceDelay, noResultsText, dropdownClass, onMentionSelect, renderItem,
8
+ * renderNoResults, renderLoading), same dropdown behavior (debounced search,
9
+ * keyboard navigation, pagination on scroll, loading/no-results states).
10
+ *
11
+ * Differences from the original package:
12
+ * - Trigger detection is delegated to the editor's built-in `TriggerManager`
13
+ * (the plugin just reacts to `onEdit` callbacks); the caret-in-span
14
+ * gymnastics from the original are unnecessary here.
15
+ * - The committed widget uses the editor's canonical inline-widget
16
+ * representation: `<span data-inline-plugin="mention" data-id="<widget>"
17
+ * data-value="<entity>" class="oe-ip oe-ip--mention">@Name</span>`.
18
+ * The shared sanitize allowlist preserves this through save / render.
19
+ *
20
+ * Styling lives in `./styles.css` and is acquired through the shared style
21
+ * registry while the owning editor is mounted.
22
+ */
23
+
24
+ /**
25
+ * @typedef {import('./index').MentionItem} MentionItem
26
+ * @typedef {import('./index').MentionSearchResult} MentionSearchResult
27
+ * @typedef {import('./index').MentionSearchFunction} MentionSearchFunction
28
+ * @typedef {import('./index').MentionRenderItem} MentionRenderItem
29
+ * @typedef {import('./index').MentionRenderNoResults} MentionRenderNoResults
30
+ * @typedef {import('./index').MentionRenderLoading} MentionRenderLoading
31
+ * @typedef {import('./index').MentionPluginOptions} MentionPluginOptions
32
+ * @typedef {import('../../core/types').InlinePlugin} InlinePlugin
33
+ * @typedef {import('../../core/types').InlinePluginContext} InlinePluginContext
34
+ */
35
+
36
+ import { resolvePath } from '../../shared/resolvePath.js'
37
+ import { injectStyleUrls } from '../../core/StyleInjector.js'
38
+ import { createMentionWidget } from './widget.js'
39
+ import { setSafeUrlAttribute } from '../../shared/sanitize/sanitizeUrl.js'
40
+
41
+ /** Absolute URL to the plugin's stylesheet. Ref-counted via `injectStyleUrls`. */
42
+ const STYLES_URL = resolvePath('./styles.css', import.meta.url)
43
+
44
+ const DEFAULTS = Object.freeze({
45
+ trigger: '@',
46
+ debounceDelay: 300,
47
+ dropdownClass: '',
48
+ searchFunction: null,
49
+ onMentionSelect: null,
50
+ renderItem: null,
51
+ renderNoResults: null,
52
+ renderLoading: null,
53
+ })
54
+
55
+ let dropdownSequence = 0
56
+ const COMBOBOX_ARIA_ATTRIBUTES = Object.freeze([
57
+ 'aria-controls',
58
+ 'aria-expanded',
59
+ 'aria-haspopup',
60
+ 'aria-autocomplete',
61
+ 'aria-activedescendant',
62
+ ])
63
+
64
+ function createElement(tag, className) {
65
+ const el = document.createElement(tag)
66
+ if (className) el.className = className
67
+ return el
68
+ }
69
+
70
+ function escapeHtml(str) {
71
+ const entities = /** @type {Record<string, string>} */ ({
72
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
73
+ })
74
+ return String(str).replace(/[&<>"']/g, s => entities[s] ?? s)
75
+ }
76
+
77
+ /**
78
+ * Keep only search entries that satisfy the public mention-item contract.
79
+ * Application-specific fields remain available to custom renderers.
80
+ * @param {unknown} raw
81
+ * @returns {{ items: MentionItem[], nextPageUrl: string | null }}
82
+ */
83
+ function normalizeSearchResult(raw) {
84
+ const source = Array.isArray(raw)
85
+ ? raw
86
+ : (raw && typeof raw === 'object' && Array.isArray(/** @type {any} */ (raw).items)
87
+ ? /** @type {any} */ (raw).items
88
+ : [])
89
+ const items = source.filter(item => (
90
+ item
91
+ && typeof item === 'object'
92
+ && (typeof item.id === 'string' || (typeof item.id === 'number' && Number.isFinite(item.id)))
93
+ && typeof item.name === 'string'
94
+ ))
95
+ const cursor = !Array.isArray(raw) && raw && typeof raw === 'object'
96
+ ? /** @type {any} */ (raw).nextPageUrl
97
+ : null
98
+ return {
99
+ items,
100
+ nextPageUrl: typeof cursor === 'string' && cursor ? cursor : null,
101
+ }
102
+ }
103
+
104
+ // ═══════════════════════════════════════════════════════════════════════════
105
+ // DropdownUI — pure DOM layer. No editor knowledge.
106
+ // Ported from @shelamkoff/mentionjs DropdownUI (1:1 behavior, new class names).
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+
109
+ class DropdownUI {
110
+ /** @param {{ dropdownClass?: string, renderItem?: MentionRenderItem | null, renderNoResults?: MentionRenderNoResults | null, renderLoading?: MentionRenderLoading | null, noResultsText: string, loadingText: string }} options */
111
+ constructor(options) {
112
+ this._options = options
113
+ /** @type {HTMLElement | null} */
114
+ this._el = null
115
+ }
116
+
117
+ get el() { return this._el }
118
+
119
+ /**
120
+ * Mount inside the owning editor so the floating surface inherits the
121
+ * editor's theme tokens, including consumer-defined themes and runtime
122
+ * class changes.
123
+ * @param {HTMLElement} container
124
+ */
125
+ mount(container) {
126
+ if (this._el) this.destroy()
127
+
128
+ const cls = ['oe-mention-dropdown', this._options.dropdownClass]
129
+ .filter(Boolean)
130
+ .join(' ')
131
+
132
+ this._el = createElement('div', cls)
133
+ this._el.id = `oe-mention-listbox-${++dropdownSequence}`
134
+ this._el.setAttribute('role', 'listbox')
135
+ container.appendChild(this._el)
136
+ return this._el
137
+ }
138
+
139
+ /**
140
+ * @param {MentionItem[]} items
141
+ * @param {number} selectedIndex
142
+ */
143
+ render(items, selectedIndex) {
144
+ if (!this._el) return
145
+ this._el.innerHTML = ''
146
+
147
+ if (items.length === 0) {
148
+ this._el.appendChild(this._buildNoResults())
149
+ return
150
+ }
151
+
152
+ items.forEach((item, index) => {
153
+ this._el.appendChild(this._buildItem(item, index, selectedIndex))
154
+ })
155
+ }
156
+
157
+ /**
158
+ * @param {MentionItem[]} newItems
159
+ * @param {number} startIndex
160
+ * @param {number} selectedIndex
161
+ */
162
+ appendItems(newItems, startIndex, selectedIndex) {
163
+ if (!this._el) return
164
+ newItems.forEach((item, i) => {
165
+ this._el?.appendChild(this._buildItem(item, startIndex + i, selectedIndex))
166
+ })
167
+ }
168
+
169
+ /**
170
+ * @param {MentionItem} data
171
+ * @param {number} index
172
+ * @param {number} selectedIndex
173
+ * @returns {HTMLElement}
174
+ */
175
+ _buildItem(data, index, selectedIndex) {
176
+ if (this._options.renderItem) {
177
+ const custom = this._options.renderItem(data, index, index === selectedIndex)
178
+ if (custom instanceof HTMLElement) {
179
+ if (!custom.classList.contains('oe-mention-item')) custom.classList.add('oe-mention-item')
180
+ if (index === selectedIndex) custom.classList.add('oe-mention-item--active')
181
+ custom.dataset.index = String(index)
182
+ custom.id = `${this._el?.id || 'oe-mention-listbox'}-option-${index}`
183
+ custom.setAttribute('role', 'option')
184
+ custom.setAttribute('aria-selected', String(index === selectedIndex))
185
+ return custom
186
+ }
187
+ }
188
+
189
+ const el = createElement('div', 'oe-mention-item' + (index === selectedIndex ? ' oe-mention-item--active' : ''))
190
+ el.dataset.index = String(index)
191
+ el.id = `${this._el?.id || 'oe-mention-listbox'}-option-${index}`
192
+ el.setAttribute('role', 'option')
193
+ el.setAttribute('aria-selected', String(index === selectedIndex))
194
+
195
+ if (data.avatar) {
196
+ const img = document.createElement('img')
197
+ setSafeUrlAttribute(img, 'src', data.avatar, 'media')
198
+ img.alt = ''
199
+ img.className = 'oe-mention-avatar'
200
+ el.appendChild(img)
201
+ } else {
202
+ const placeholder = createElement('div', 'oe-mention-avatar-placeholder')
203
+ placeholder.textContent = (data.name || '?').charAt(0).toUpperCase()
204
+ el.appendChild(placeholder)
205
+ }
206
+
207
+ const info = createElement('div', 'oe-mention-info')
208
+ const name = createElement('div', 'oe-mention-name')
209
+ name.textContent = data.name || ''
210
+ info.appendChild(name)
211
+
212
+ if (data.details) {
213
+ const details = createElement('div', 'oe-mention-details')
214
+ details.textContent = data.details
215
+ info.appendChild(details)
216
+ }
217
+
218
+ el.appendChild(info)
219
+ return el
220
+ }
221
+
222
+ _buildNoResults() {
223
+ if (this._options.renderNoResults) {
224
+ const custom = this._options.renderNoResults(this._options.noResultsText)
225
+ if (custom instanceof HTMLElement) {
226
+ custom.classList.add('oe-mention-no-results')
227
+ custom.setAttribute('role', 'status')
228
+ return custom
229
+ }
230
+ }
231
+
232
+ const el = createElement('div', 'oe-mention-item oe-mention-no-results')
233
+ el.setAttribute('role', 'status')
234
+ el.innerHTML = `
235
+ <div class="oe-mention-avatar-placeholder">?</div>
236
+ <div class="oe-mention-info">
237
+ <div class="oe-mention-name">${escapeHtml(this._options.noResultsText)}</div>
238
+ </div>
239
+ `
240
+ return el
241
+ }
242
+
243
+ /** @param {number} selectedIndex */
244
+ updateSelection(selectedIndex) {
245
+ if (!this._el) return
246
+ this._el.querySelectorAll('.oe-mention-item[data-index]').forEach((item) => {
247
+ const i = parseInt(/** @type {HTMLElement} */ (item).dataset.index || '-1', 10)
248
+ item.classList.toggle('oe-mention-item--active', i === selectedIndex)
249
+ item.setAttribute('aria-selected', String(i === selectedIndex))
250
+ })
251
+ }
252
+
253
+ scrollToActive() {
254
+ if (!this._el) return
255
+ const active = this._el.querySelector('.oe-mention-item--active')
256
+ if (active) active.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
257
+ }
258
+
259
+ showLoading() {
260
+ if (!this._el || this._el.querySelector('.oe-mention-loading')) return
261
+
262
+ if (this._options.renderLoading) {
263
+ const custom = this._options.renderLoading()
264
+ if (custom instanceof HTMLElement) {
265
+ if (!custom.classList.contains('oe-mention-loading')) custom.classList.add('oe-mention-loading')
266
+ custom.setAttribute('role', 'status')
267
+ this._el.appendChild(custom)
268
+ return
269
+ }
270
+ }
271
+
272
+ const loader = createElement('div', 'oe-mention-loading')
273
+ loader.setAttribute('role', 'status')
274
+ loader.innerHTML = `
275
+ <div class="oe-mention-item">
276
+ <div class="oe-mention-info"><div class="oe-mention-name">${escapeHtml(this._options.loadingText)}</div></div>
277
+ </div>
278
+ `
279
+ this._el.appendChild(loader)
280
+ }
281
+
282
+ hideLoading() {
283
+ if (!this._el) return
284
+ const loader = this._el.querySelector('.oe-mention-loading')
285
+ if (loader) loader.remove()
286
+ }
287
+
288
+ /** @param {{ top: number, left: number, cursorY: number, lineHeight?: number }} p */
289
+ position(p) {
290
+ if (!this._el) return
291
+ requestAnimationFrame(() => this._positionRaw(p))
292
+ }
293
+
294
+ /** @param {{ top: number, left: number, cursorY: number, lineHeight?: number }} p */
295
+ positionRaw(p) {
296
+ if (!this._el) return
297
+ this._positionRaw(p)
298
+ }
299
+
300
+ /** @param {{ top: number, left: number, cursorY: number, lineHeight?: number }} p */
301
+ _positionRaw({ top, left, cursorY, lineHeight = 20 }) {
302
+ if (!this._el) return
303
+
304
+ this._el.style.top = top + 'px'
305
+ this._el.style.left = left + 'px'
306
+
307
+ const rect = this._el.getBoundingClientRect()
308
+ const vw = window.innerWidth
309
+ const vh = window.innerHeight
310
+ let newLeft = left
311
+ let newTop = top
312
+
313
+ if (rect.right > vw - 10) newLeft = Math.max(10, left - (rect.right - vw + 10))
314
+ if (rect.left < 10) newLeft = 10
315
+
316
+ if (rect.bottom > vh - 10 && cursorY > vh / 2) {
317
+ newTop = top - this._el.offsetHeight - lineHeight
318
+ }
319
+
320
+ this._el.style.top = newTop + 'px'
321
+ this._el.style.left = newLeft + 'px'
322
+ this._el.classList.add('oe-mention-dropdown--active')
323
+ }
324
+
325
+ hide() {
326
+ if (this._el) this._el.classList.remove('oe-mention-dropdown--active')
327
+ }
328
+
329
+ destroy() {
330
+ if (this._el) {
331
+ this._el.remove()
332
+ this._el = null
333
+ }
334
+ }
335
+ }
336
+
337
+ // ═══════════════════════════════════════════════════════════════════════════
338
+ // Factory
339
+ // ═══════════════════════════════════════════════════════════════════════════
340
+
341
+ /**
342
+ * Create an editor-scoped mention widget plugin. The returned instance owns
343
+ * its dropdown and listeners after `mount()` and releases them in `destroy()`.
344
+ * @param {MentionPluginOptions} [options]
345
+ * @returns {InlinePlugin}
346
+ */
347
+ export function createMentionPlugin(options = {}) {
348
+ const opts = Object.assign({}, DEFAULTS, options)
349
+
350
+ // Editor-scoped resources are acquired later by mount(). Construction is
351
+ // deliberately side-effect free so failed registration cannot leak them.
352
+ /** @type {{ destroy(): void } | null} */
353
+ let styleHandle = null
354
+ /** @type {HTMLElement | null} */
355
+ let rootElement = null
356
+
357
+ // handleBeforeInput is attached to the owning editor root by mount().
358
+
359
+ /** @type {import('../../core/types').IScopedI18n | null} */
360
+ let i18n = null
361
+ /**
362
+ * Scoped i18n lookup — the editor passes an I18n instance pre-scoped to
363
+ * `inlinePlugin.mention.*`, so we use short keys (`'title'`, `'noResults'`,
364
+ * `'loading'`). Falls back to the provided English string when no i18n
365
+ * instance has been injected yet.
366
+ * @param {string} key
367
+ * @param {string} fallback
368
+ * @returns {string}
369
+ */
370
+ const t = (key, fallback) => {
371
+ if (!i18n) return fallback
372
+ if (!i18n.has(key)) return fallback
373
+ const v = i18n.t(key)
374
+ return (v === undefined || v === null || v === '') ? fallback : v
375
+ }
376
+
377
+ /**
378
+ * Active session — either a "fresh" trigger (user just typed `@` in plain
379
+ * text) or an "edit" session (user re-opened a committed pill by typing /
380
+ * deleting inside it). Both modes share the dropdown UI, keyboard nav,
381
+ * search pipeline, and outside-click handling.
382
+ *
383
+ * - `mode: 'fresh'` — anchor is a text node + offset in the parent block.
384
+ * The typed `@query` lives inline in that text node; commit replaces
385
+ * the range with a new widget span.
386
+ * - `mode: 'edit'` — anchor is an EXISTING committed span that the user
387
+ * is mutating. The span itself carries the query (`span.textContent`
388
+ * without the leading trigger); commit rewrites the span in place.
389
+ *
390
+ * @typedef {{
391
+ * mode: 'fresh' | 'edit',
392
+ * parentEl: HTMLElement,
393
+ * triggerTextNode: Text | null,
394
+ * triggerOffset: number,
395
+ * span: HTMLElement | null,
396
+ * dropdown: DropdownUI,
397
+ * keydownHandler: (e: KeyboardEvent) => void,
398
+ * selectionChangeHandler: () => void,
399
+ * outsideMouseDownHandler: (e: MouseEvent) => void,
400
+ * dropdownClickHandler: (e: MouseEvent) => void,
401
+ * dropdownMouseDownHandler: (e: MouseEvent) => void,
402
+ * dropdownScrollHandler: () => void,
403
+ * scrollResizeHandler: () => void,
404
+ * selectedIndex: number,
405
+ * results: MentionItem[],
406
+ * nextPageUrl: string | null,
407
+ * isLoadingMore: boolean,
408
+ * currentQuery: string,
409
+ * debounceTimer: ReturnType<typeof setTimeout> | null,
410
+ * debounceReject: ((err: Error) => void) | null,
411
+ * searchController: AbortController | null,
412
+ * searchRequestId: number,
413
+ * ctx: InlinePluginContext,
414
+ * committedInThisSession: boolean,
415
+ * ariaState: Map<string, string | null>,
416
+ * }} Session
417
+ */
418
+
419
+ /** @type {Session | null} */
420
+ let session = null
421
+
422
+ /** @param {HTMLElement} element @returns {Map<string, string | null>} */
423
+ function captureComboboxAria(element) {
424
+ return new Map(COMBOBOX_ARIA_ATTRIBUTES.map(name => [name, element.getAttribute(name)]))
425
+ }
426
+
427
+ /** @param {HTMLElement} element @param {Map<string, string | null>} state */
428
+ function restoreComboboxAria(element, state) {
429
+ for (const [name, value] of state) {
430
+ if (value === null) element.removeAttribute(name)
431
+ else element.setAttribute(name, value)
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Last-known InlinePluginContext. `onEdit` / `hydrate` stash it here so
437
+ * edit-mode mutations (triggered via `beforeinput` on committed pills,
438
+ * outside of a TriggerManager-driven flow) can still call `notifyChanged`.
439
+ * @type {InlinePluginContext | null}
440
+ */
441
+ let globalCtx = null
442
+
443
+ // ─── Caret / DOM helpers (ported verbatim from mentionjs) ──────────────
444
+
445
+ /**
446
+ * @param {Node} node
447
+ * @param {number} offset
448
+ */
449
+ function setCaretAt(node, offset) {
450
+ const sel = window.getSelection()
451
+ if (!sel) return
452
+ const range = document.createRange()
453
+ range.setStart(node, offset)
454
+ range.collapse(true)
455
+ sel.removeAllRanges()
456
+ sel.addRange(range)
457
+ }
458
+
459
+ /** @param {Node} node */
460
+ function setCaretAfterNode(node) {
461
+ const sel = window.getSelection()
462
+ if (!sel) return
463
+ const range = document.createRange()
464
+ range.setStartAfter(node)
465
+ range.collapse(true)
466
+ sel.removeAllRanges()
467
+ sel.addRange(range)
468
+ }
469
+
470
+ /** @param {Node} node */
471
+ function setCaretBeforeNode(node) {
472
+ const sel = window.getSelection()
473
+ if (!sel) return
474
+ const range = document.createRange()
475
+ range.setStartBefore(node)
476
+ range.collapse(true)
477
+ sel.removeAllRanges()
478
+ sel.addRange(range)
479
+ }
480
+
481
+ /** @param {string} str @param {number} offset @returns {number} */
482
+ function codePointStartBefore(str, offset) {
483
+ const end = Math.max(0, Math.min(offset, str.length))
484
+ if (end >= 2) {
485
+ const low = str.charCodeAt(end - 1)
486
+ const high = str.charCodeAt(end - 2)
487
+ if (low >= 0xDC00 && low <= 0xDFFF && high >= 0xD800 && high <= 0xDBFF) return end - 2
488
+ }
489
+ return Math.max(0, end - 1)
490
+ }
491
+
492
+ /** @param {string} str @param {number} offset @returns {number} */
493
+ function codePointEndAt(str, offset) {
494
+ const start = Math.max(0, Math.min(offset, str.length))
495
+ if (start + 1 < str.length) {
496
+ const high = str.charCodeAt(start)
497
+ const low = str.charCodeAt(start + 1)
498
+ if (high >= 0xD800 && high <= 0xDBFF && low >= 0xDC00 && low <= 0xDFFF) return start + 2
499
+ }
500
+ return Math.min(str.length, start + 1)
501
+ }
502
+
503
+ /**
504
+ * Find the nearest committed/editing mention span ancestor of the current
505
+ * selection's anchor node, or `null` if the caret is not inside one.
506
+ * Mirrors mentionjs `_getMentionSpan`.
507
+ * @returns {HTMLElement | null}
508
+ */
509
+ function findMentionSpanAtCaret() {
510
+ const sel = window.getSelection()
511
+ if (!sel || !sel.anchorNode) return null
512
+ const node = sel.anchorNode
513
+ if (node.nodeType === Node.ELEMENT_NODE) {
514
+ const el = /** @type {HTMLElement} */ (node)
515
+ if (el.matches?.('[data-inline-plugin="mention"]')) return el
516
+ return el.closest?.('[data-inline-plugin="mention"]') ?? null
517
+ }
518
+ const parent = node.parentElement
519
+ if (!parent) return null
520
+ return parent.closest('[data-inline-plugin="mention"]')
521
+ }
522
+
523
+ /** @param {Node} el */
524
+ function inDOM(el) {
525
+ return !!el && document.contains(el)
526
+ }
527
+
528
+ /**
529
+ * Backspace at offset 0 inside a span — delete one char from the PREVIOUS
530
+ * sibling (or the previous mention, which we re-activate for further
531
+ * editing). Mirrors mentionjs `_backspaceBeforeSpan`.
532
+ * @param {HTMLElement} span
533
+ */
534
+ function backspaceBeforeSpan(span) {
535
+ const prev = span.previousSibling
536
+ if (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent && prev.textContent.length > 0) {
537
+ prev.textContent = prev.textContent.slice(0, codePointStartBefore(prev.textContent, prev.textContent.length))
538
+ if (prev.textContent.length === 0) prev.parentNode?.removeChild(prev)
539
+ setCaretBeforeNode(span)
540
+ return
541
+ }
542
+ if (prev && prev.nodeType === Node.ELEMENT_NODE) {
543
+ const prevEl = /** @type {HTMLElement} */ (prev)
544
+ if (prevEl.matches('[data-inline-plugin="mention"]')) {
545
+ // Walk into the previous committed mention for further editing.
546
+ const text = prevEl.firstChild
547
+ if (text?.nodeType === Node.TEXT_NODE) {
548
+ setCaretAt(text, text.textContent?.length ?? 0)
549
+ }
550
+ openEditSessionForSpan(prevEl)
551
+ return
552
+ }
553
+ // Some other inline widget — remove it whole.
554
+ prevEl.parentNode?.removeChild(prevEl)
555
+ setCaretBeforeNode(span)
556
+ return
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Forward-delete at offset 0 inside a span — treat as "delete the trigger
562
+ * character". If span text is only the trigger, remove the whole span;
563
+ * otherwise unwrap into plain text minus the trigger. Mirrors
564
+ * mentionjs `_deleteForwardInSpan`.
565
+ * @param {HTMLElement} span
566
+ * @param {string} spanText
567
+ */
568
+ function deleteForwardInSpan(span, spanText) {
569
+ if (spanText === opts.trigger) {
570
+ const next = span.nextSibling
571
+ span.parentNode?.removeChild(span)
572
+ if (next) setCaretBeforeNode(next)
573
+ } else {
574
+ const remaining = spanText.substring(opts.trigger.length)
575
+ const tn = document.createTextNode(remaining)
576
+ span.parentNode?.insertBefore(tn, span)
577
+ span.parentNode?.removeChild(span)
578
+ setCaretAt(tn, 0)
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Forward-delete at end of a node — remove the first char of the next
584
+ * sibling, or drop an empty/element sibling entirely. Mirrors mentionjs
585
+ * `_deleteForwardAfterNode`.
586
+ * @param {Node} node
587
+ */
588
+ function deleteForwardAfterNode(node) {
589
+ const next = node.nextSibling
590
+ if (!next) return
591
+ if (next.nodeType === Node.TEXT_NODE && next.textContent && next.textContent.length > 0) {
592
+ next.textContent = next.textContent.substring(codePointEndAt(next.textContent, 0))
593
+ if (next.textContent.length === 0) next.parentNode?.removeChild(next)
594
+ } else {
595
+ next.parentNode?.removeChild(next)
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Insert a line-break at the current caret — used when Enter is pressed
601
+ * inside a committed span (no active search). Mirrors mentionjs
602
+ * `_insertBr`.
603
+ * @param {Selection} sel
604
+ */
605
+ function insertBr(sel) {
606
+ if (!sel.rangeCount) return
607
+ const range = sel.getRangeAt(0)
608
+ range.deleteContents()
609
+ const br = document.createElement('br')
610
+ range.insertNode(br)
611
+ range.setStartAfter(br)
612
+ range.collapse(true)
613
+ sel.removeAllRanges()
614
+ sel.addRange(range)
615
+ }
616
+
617
+ // ─── Search pipeline (1:1 with mentionjs) ──────────────────────────────
618
+
619
+ /**
620
+ * Runs `searchFunction` with debounce + request-id cancellation.
621
+ * Returns the fresh items on success, or `null` if superseded / cancelled.
622
+ * @param {string} query
623
+ * @param {string | null} [nextPageUrl]
624
+ * @returns {Promise<MentionItem[] | null>}
625
+ */
626
+ async function runSearch(query, nextPageUrl = null) {
627
+ if (!session) return null
628
+
629
+ session.searchController?.abort()
630
+ const controller = new AbortController()
631
+ session.searchController = controller
632
+
633
+ if (!nextPageUrl) {
634
+ session.currentQuery = query
635
+ if (session.debounceReject) {
636
+ session.debounceReject(new Error('cancelled'))
637
+ session.debounceReject = null
638
+ }
639
+ if (session.debounceTimer) {
640
+ clearTimeout(session.debounceTimer)
641
+ session.debounceTimer = null
642
+ }
643
+ }
644
+
645
+ const requestId = ++session.searchRequestId
646
+ const capturedSession = session
647
+
648
+ const execute = async () => {
649
+ if (!opts.searchFunction) return /** @type {MentionSearchResult} */ ({ items: [], nextPageUrl: null })
650
+ return await opts.searchFunction(query, nextPageUrl, { signal: controller.signal })
651
+ }
652
+
653
+ /** @type {MentionSearchResult | MentionItem[] | undefined} */
654
+ let raw
655
+ try {
656
+ if (!nextPageUrl && query.trim() !== '') {
657
+ raw = await new Promise((resolve, reject) => {
658
+ capturedSession.debounceReject = reject
659
+ capturedSession.debounceTimer = setTimeout(() => {
660
+ capturedSession.debounceReject = null
661
+ execute().then(resolve).catch(reject)
662
+ }, opts.debounceDelay ?? 300)
663
+ })
664
+ } else {
665
+ raw = await execute()
666
+ }
667
+ } catch (err) {
668
+ if (capturedSession.searchController === controller) capturedSession.searchController = null
669
+ if (controller.signal.aborted || /** @type {Error} */ (err)?.name === 'AbortError' || /** @type {Error} */ (err)?.message === 'cancelled') return null
670
+ // eslint-disable-next-line no-console
671
+ console.warn('[mention-plugin] search failed:', err)
672
+ return null
673
+ }
674
+
675
+ if (capturedSession.searchController === controller) capturedSession.searchController = null
676
+ if (!session || session !== capturedSession || controller.signal.aborted || requestId !== session.searchRequestId) return null
677
+
678
+ const normalized = normalizeSearchResult(raw)
679
+ const items = normalized.items
680
+ const nextUrl = normalized.nextPageUrl
681
+
682
+ session.nextPageUrl = nextUrl
683
+
684
+ if (nextPageUrl) {
685
+ session.results = [...session.results, ...items]
686
+ } else {
687
+ session.results = items
688
+ session.selectedIndex = 0
689
+ }
690
+
691
+ return items
692
+ }
693
+
694
+ /** Show / refresh the dropdown with current session results. */
695
+ function openOrUpdateDropdown(items) {
696
+ if (!session) return null
697
+
698
+ const wasMounted = !!session.dropdown.el
699
+ if (!wasMounted) {
700
+ session.dropdown.mount(rootElement || session.parentEl)
701
+ bindDropdownEvents()
702
+ bindScrollResize()
703
+ }
704
+
705
+ session.dropdown.hide()
706
+ session.dropdown.render(items, session.selectedIndex)
707
+ syncDropdownAria()
708
+ repositionDropdown()
709
+ }
710
+
711
+ /** Keep the owning editable element synchronized with listbox selection. */
712
+ function syncDropdownAria() {
713
+ if (!session?.dropdown.el) return
714
+ const parentEl = session.parentEl
715
+ const active = session.dropdown.el.querySelector('.oe-mention-item--active[role="option"]')
716
+ parentEl.setAttribute('aria-controls', session.dropdown.el.id)
717
+ parentEl.setAttribute('aria-expanded', 'true')
718
+ parentEl.setAttribute('aria-haspopup', 'listbox')
719
+ parentEl.setAttribute('aria-autocomplete', 'list')
720
+ if (active?.id) parentEl.setAttribute('aria-activedescendant', active.id)
721
+ else parentEl.removeAttribute('aria-activedescendant')
722
+ }
723
+
724
+ // ─── Positioning ───────────────────────────────────────────────────────
725
+
726
+ /**
727
+ * Build a DOMRect for the dropdown anchor. For fresh sessions, that's the
728
+ * range `[@triggerOffset .. currentCaret]` inside `triggerTextNode`. For
729
+ * edit sessions, it's the span's own bounding rect (the pill itself).
730
+ * @returns {DOMRect | null}
731
+ */
732
+ function getTriggerRect() {
733
+ if (!session) return null
734
+
735
+ if (session.mode === 'edit') {
736
+ const span = session.span
737
+ if (!span || !inDOM(span)) return null
738
+ return span.getBoundingClientRect()
739
+ }
740
+
741
+ const tn = session.triggerTextNode
742
+ if (!tn) return null
743
+
744
+ const sel = window.getSelection()
745
+ if (!sel || !sel.rangeCount) return null
746
+ const anchorNode = sel.anchorNode
747
+ const anchorOffset = sel.anchorOffset
748
+
749
+ const range = document.createRange()
750
+ try {
751
+ range.setStart(tn, session.triggerOffset)
752
+ if (anchorNode && anchorNode === tn) {
753
+ range.setEnd(anchorNode, anchorOffset)
754
+ } else {
755
+ range.setEnd(tn, Math.min(session.triggerOffset + opts.trigger.length, tn.data.length))
756
+ }
757
+ } catch {
758
+ return null
759
+ }
760
+ return range.getBoundingClientRect()
761
+ }
762
+
763
+ function repositionDropdown() {
764
+ if (!session || !session.dropdown.el) return
765
+ const rect = getTriggerRect()
766
+ if (!rect) {
767
+ closeSession()
768
+ return
769
+ }
770
+ session.dropdown.position({
771
+ top: rect.bottom + 4,
772
+ left: rect.left,
773
+ cursorY: rect.bottom,
774
+ lineHeight: rect.height || 20,
775
+ })
776
+ }
777
+
778
+ // ─── Commit / cancel ───────────────────────────────────────────────────
779
+
780
+ /**
781
+ * Commit a picked item. Dispatches on `session.mode`:
782
+ * - 'fresh' — replace the typed `@query` text range with a new widget;
783
+ * - 'edit' — rewrite the already-committed span in place.
784
+ *
785
+ * Both paths run inside the owning block's mutation boundary.
786
+ *
787
+ * @param {MentionItem} data
788
+ */
789
+ function commitMention(data) {
790
+ if (!session) return
791
+
792
+ // Cache everything off `session` up front — DOM mutations fire
793
+ // `selectionchange`, which (in spec-compliant browsers) is async, but in
794
+ // some engines may land synchronously and tear the session down before
795
+ // we get to `notifyChanged()`.
796
+ const ctx = session.ctx
797
+ const parentEl = session.parentEl
798
+ let committed = false
799
+
800
+ ctx.mutate(parentEl, () => {
801
+ if (session?.mode === 'edit') {
802
+ committed = commitEditMention(data, ctx)
803
+ } else if (session) {
804
+ committed = commitFreshMention(data, ctx)
805
+ }
806
+ })
807
+
808
+ if (committed && typeof opts.onMentionSelect === 'function') {
809
+ try { opts.onMentionSelect({ id: data.id, name: data.name }) } catch { /* noop */ }
810
+ }
811
+ closeSession()
812
+ }
813
+
814
+ /**
815
+ * Replace the `@query` range with a committed mention widget.
816
+ *
817
+ * Primary path: `triggerTextNode` is still in the DOM — rebuild its
818
+ * surrounding content using `parent.replaceChild(fragment, triggerNode)`
819
+ * with a DocumentFragment holding `[before-text?, widget, space,
820
+ * after-text?]`. More reliable than `Range.deleteContents + insertNode`
821
+ * when the text node ends up empty after the delete (browsers handle
822
+ * splitting an empty Text node inconsistently).
823
+ *
824
+ * Fallback: `triggerTextNode` has been detached (rare — would require
825
+ * a concurrent DOM mutation) — insert the widget at the current caret.
826
+ *
827
+ * @param {MentionItem} data
828
+ * @param {InlinePluginContext} _ctx
829
+ * @returns {boolean} Whether the widget was inserted into the document.
830
+ */
831
+ function commitFreshMention(data, _ctx) {
832
+ if (!session) return false
833
+ const triggerNode = session.triggerTextNode
834
+ const triggerOffset = session.triggerOffset
835
+ const currentQueryLen = session.currentQuery.length
836
+
837
+ const widget = createWidget({ id: String(data.id), name: String(data.name) })
838
+ const space = document.createTextNode('\u00A0')
839
+ const sel = window.getSelection()
840
+
841
+ // ─── Primary path: triggerNode still in DOM ────────────────────────
842
+ if (triggerNode && inDOM(triggerNode)) {
843
+ // End offset defaults to `@trigger + query length`; if the caret is
844
+ // still anchored in triggerNode, prefer its live position.
845
+ let endOffset = triggerOffset + opts.trigger.length + currentQueryLen
846
+ if (sel && sel.rangeCount > 0 && sel.anchorNode === triggerNode
847
+ && typeof sel.anchorOffset === 'number') {
848
+ endOffset = sel.anchorOffset
849
+ }
850
+ endOffset = Math.min(Math.max(endOffset, triggerOffset), triggerNode.data.length)
851
+
852
+ const beforeText = triggerNode.data.substring(0, triggerOffset)
853
+ const afterText = triggerNode.data.substring(endOffset)
854
+ const parent = triggerNode.parentNode
855
+
856
+ if (parent) {
857
+ const frag = document.createDocumentFragment()
858
+ if (beforeText) frag.appendChild(document.createTextNode(beforeText))
859
+ frag.appendChild(widget)
860
+ frag.appendChild(space)
861
+ if (afterText) frag.appendChild(document.createTextNode(afterText))
862
+ parent.replaceChild(frag, triggerNode)
863
+ setCaretAt(space, 1)
864
+ return true
865
+ }
866
+ }
867
+
868
+ // ─── Fallback: insert at current caret ─────────────────────────────
869
+ if (!sel || !sel.rangeCount) return false
870
+ const range = sel.getRangeAt(0)
871
+ const rangeContainer = range.commonAncestorContainer
872
+ if (rangeContainer !== session.parentEl && !session.parentEl.contains(rangeContainer)) return false
873
+ try {
874
+ range.insertNode(widget)
875
+ } catch {
876
+ return false
877
+ }
878
+ widget.after(space)
879
+ setCaretAt(space, 1)
880
+ return inDOM(widget)
881
+ }
882
+
883
+ /**
884
+ * Rewrite an already-committed span in place with the newly picked item.
885
+ * Mirrors mentionjs `_commitSpanMention`: updates text, removes the
886
+ * active/editing class, and writes the fresh data-value. Caret lands in
887
+ * the trailing space sibling (creating one if missing).
888
+ *
889
+ * @param {MentionItem} data
890
+ * @param {InlinePluginContext} _ctx
891
+ * @returns {boolean} Whether the existing widget was updated.
892
+ */
893
+ function commitEditMention(data, _ctx) {
894
+ if (!session || !session.span) return false
895
+ const span = session.span
896
+ if (!inDOM(span)) return false
897
+
898
+ // Mark the session as having produced a real commit. The close path
899
+ // uses this to decide whether to leave the span intact (committed) or
900
+ // unwrap it to plain text (user edited but bailed — `data-value` no
901
+ // longer matches the displayed name, see README / the task brief).
902
+ session.committedInThisSession = true
903
+
904
+ span.textContent = opts.trigger + data.name
905
+ span.classList.remove('oe-ip--mention--editing')
906
+ span.setAttribute('data-value', String(data.id))
907
+
908
+ const next = span.nextSibling
909
+ const nextText = next && next.nodeType === Node.TEXT_NODE ? (next.textContent || '') : ''
910
+ const hasSpace = nextText.length > 0 && /^[\s\u00A0]/.test(nextText)
911
+
912
+ if (hasSpace && next) {
913
+ setCaretAt(next, 1)
914
+ } else {
915
+ const space = document.createTextNode('\u00A0')
916
+ span.after(space)
917
+ setCaretAt(space, 1)
918
+ }
919
+ return true
920
+ }
921
+
922
+ /**
923
+ * Release transient session resources without deciding how an edited
924
+ * mention should leave the document. DOM-edit branches use the returned
925
+ * session to remove or replace the live span themselves; ordinary cancel
926
+ * paths pass it to `finalizeSession`.
927
+ * @returns {Session | null}
928
+ */
929
+ function detachSession() {
930
+ if (!session) return
931
+
932
+ // Cancel in-flight debounce / search.
933
+ if (session.debounceReject) {
934
+ session.debounceReject(new Error('cancelled'))
935
+ session.debounceReject = null
936
+ }
937
+ if (session.debounceTimer) {
938
+ clearTimeout(session.debounceTimer)
939
+ session.debounceTimer = null
940
+ }
941
+ session.searchController?.abort()
942
+ session.searchController = null
943
+ session.searchRequestId++
944
+
945
+ // Tear down UI + listeners.
946
+ restoreComboboxAria(session.parentEl, session.ariaState)
947
+ unbindDropdownEvents()
948
+ unbindScrollResize()
949
+ session.dropdown.destroy()
950
+
951
+ document.removeEventListener('keydown', session.keydownHandler, true)
952
+ document.removeEventListener('selectionchange', session.selectionChangeHandler)
953
+ document.removeEventListener('mousedown', session.outsideMouseDownHandler, true)
954
+ const closingSession = session
955
+ session = null
956
+
957
+ return closingSession
958
+ }
959
+
960
+ /**
961
+ * Apply the normal semantic result of leaving an edit session.
962
+ * @param {Session} closingSession
963
+ */
964
+ function finalizeSession(closingSession) {
965
+
966
+ // Edit-mode exit handling:
967
+ //
968
+ // - If the user COMMITTED a pick (Enter / click / Tab), the span
969
+ // already carries a consistent `data-value` + displayed name — we
970
+ // just drop the editing class (commit path already did, but this
971
+ // is idempotent).
972
+ //
973
+ // - If the user EDITED and then bailed out (Escape / outside click /
974
+ // caret moved elsewhere), the span's displayed text no longer
975
+ // matches its `data-value`. Leaving it as a pill would bake stale
976
+ // identity into the block-level `inline` map. We unwrap the span
977
+ // into a plain text node with the same textual content — the
978
+ // mention effectively stops being a mention, which is the
979
+ // semantically correct outcome ("user started editing but didn't
980
+ // pick anyone").
981
+ if (closingSession.mode === 'edit' && closingSession.span && inDOM(closingSession.span)) {
982
+ const span = closingSession.span
983
+ if (closingSession.committedInThisSession) {
984
+ span.classList.remove('oe-ip--mention--editing')
985
+ } else {
986
+ const text = span.textContent || ''
987
+ mutateContent(closingSession.parentEl, () => {
988
+ const tn = document.createTextNode(text)
989
+ const parent = span.parentNode
990
+ if (!parent) return
991
+ parent.insertBefore(tn, span)
992
+ parent.removeChild(span)
993
+ // Place caret at end of the unwrapped text — this matches the
994
+ // user's likely mental position after a bail-out backspace.
995
+ setCaretAt(tn, text.length)
996
+ })
997
+ }
998
+ }
999
+ }
1000
+
1001
+ function closeSession() {
1002
+ const closingSession = detachSession()
1003
+ if (closingSession) finalizeSession(closingSession)
1004
+ }
1005
+
1006
+ /**
1007
+ * Close only the transient UI before a `beforeinput` branch removes or
1008
+ * replaces the editing span in the same synchronous mutation. Running the
1009
+ * normal finalizer here would first unwrap the span and leave the caller
1010
+ * holding a detached node.
1011
+ * @param {HTMLElement} span
1012
+ */
1013
+ function closeSessionForSpanReplacement(span) {
1014
+ const closingSession = detachSession()
1015
+ if (closingSession?.span === span) span.classList.remove('oe-ip--mention--editing')
1016
+ }
1017
+
1018
+ // ─── Keyboard / mouse plumbing ─────────────────────────────────────────
1019
+
1020
+ /** @param {KeyboardEvent} e */
1021
+ function handleKeydown(e) {
1022
+ if (!session) return
1023
+
1024
+ // Only intercept keys that originated inside the block where the trigger
1025
+ // was typed (or inside our dropdown — click-focus never leaves the block,
1026
+ // but stay defensive). A global Escape or Arrow in a modal / sidebar must
1027
+ // not affect us.
1028
+ const target = /** @type {Node | null} */ (e.target)
1029
+ const inBlock = !!target && session.parentEl.contains(target)
1030
+ const inDropdown = !!target && !!session.dropdown.el?.contains(target)
1031
+ if (!inBlock && !inDropdown) return
1032
+
1033
+ const max = session.results.length - 1
1034
+
1035
+ if (e.key === 'Escape') {
1036
+ e.preventDefault()
1037
+ e.stopPropagation()
1038
+ closeSession()
1039
+ return
1040
+ }
1041
+
1042
+ if (max < 0) return
1043
+
1044
+ switch (e.key) {
1045
+ case 'ArrowDown':
1046
+ e.preventDefault()
1047
+ e.stopPropagation()
1048
+ session.selectedIndex = Math.min(session.selectedIndex + 1, max)
1049
+ session.dropdown.updateSelection(session.selectedIndex)
1050
+ syncDropdownAria()
1051
+ session.dropdown.scrollToActive()
1052
+ maybeLoadMore()
1053
+ break
1054
+ case 'ArrowUp':
1055
+ e.preventDefault()
1056
+ e.stopPropagation()
1057
+ session.selectedIndex = Math.max(session.selectedIndex - 1, 0)
1058
+ session.dropdown.updateSelection(session.selectedIndex)
1059
+ syncDropdownAria()
1060
+ session.dropdown.scrollToActive()
1061
+ break
1062
+ case 'Enter':
1063
+ case 'Tab':
1064
+ if (session.results[session.selectedIndex]) {
1065
+ e.preventDefault()
1066
+ e.stopPropagation()
1067
+ commitMention(session.results[session.selectedIndex])
1068
+ }
1069
+ break
1070
+ }
1071
+ }
1072
+
1073
+ function handleSelectionChange() {
1074
+ if (!session) return
1075
+ const sel = window.getSelection()
1076
+ if (!sel || !sel.anchorNode) return
1077
+
1078
+ if (session.mode === 'edit') {
1079
+ // Edit sessions persist as long as the caret is inside the editing
1080
+ // span. The span itself stays in the DOM across edits, so
1081
+ // `span.contains(anchor)` is the right test — accounts for the caret
1082
+ // being in the span's text child or directly on the span element.
1083
+ const span = session.span
1084
+ if (!span || !inDOM(span)) { closeSession(); return }
1085
+ if (!span.contains(sel.anchorNode) && sel.anchorNode !== span) {
1086
+ closeSession()
1087
+ }
1088
+ return
1089
+ }
1090
+
1091
+ // Fresh mode: caret must stay in the trigger text node at or past the
1092
+ // end of the trigger. `trigger` is one Unicode code point, but may span
1093
+ // two UTF-16 code units (for example an emoji).
1094
+ const tn = session.triggerTextNode
1095
+ if (!tn) { closeSession(); return }
1096
+ if (sel.anchorNode !== tn) { closeSession(); return }
1097
+ if (sel.anchorOffset < session.triggerOffset + opts.trigger.length) closeSession()
1098
+ }
1099
+
1100
+ /** @param {MouseEvent} e */
1101
+ function handleOutsideMouseDown(e) {
1102
+ if (!session) return
1103
+ const target = /** @type {Node} */ (e.target)
1104
+ if (session.parentEl.contains(target)) return
1105
+ if (session.dropdown.el?.contains(target)) return
1106
+ closeSession()
1107
+ }
1108
+
1109
+ function bindDropdownEvents() {
1110
+ if (!session || !session.dropdown.el) return
1111
+ const el = session.dropdown.el
1112
+
1113
+ session.dropdownClickHandler = (e) => {
1114
+ if (!session) return
1115
+ const target = /** @type {HTMLElement} */ (e.target)
1116
+ const item = target.closest('.oe-mention-item[data-index]')
1117
+ if (!item) return
1118
+ const idx = parseInt(/** @type {HTMLElement} */ (item).dataset.index || '-1', 10)
1119
+ const data = session.results[idx]
1120
+ if (data) commitMention(data)
1121
+ }
1122
+ session.dropdownMouseDownHandler = (e) => e.preventDefault() // keep caret focus
1123
+ session.dropdownScrollHandler = () => onDropdownScroll()
1124
+
1125
+ el.addEventListener('click', session.dropdownClickHandler)
1126
+ el.addEventListener('mousedown', session.dropdownMouseDownHandler)
1127
+ el.addEventListener('scroll', session.dropdownScrollHandler)
1128
+ }
1129
+
1130
+ function unbindDropdownEvents() {
1131
+ if (!session || !session.dropdown.el) return
1132
+ const el = session.dropdown.el
1133
+ if (session.dropdownClickHandler) el.removeEventListener('click', session.dropdownClickHandler)
1134
+ if (session.dropdownMouseDownHandler) el.removeEventListener('mousedown', session.dropdownMouseDownHandler)
1135
+ if (session.dropdownScrollHandler) el.removeEventListener('scroll', session.dropdownScrollHandler)
1136
+ }
1137
+
1138
+ function bindScrollResize() {
1139
+ if (!session) return
1140
+ session.scrollResizeHandler = () => repositionDropdown()
1141
+ document.addEventListener('scroll', session.scrollResizeHandler, { passive: true, capture: true })
1142
+ window.addEventListener('resize', session.scrollResizeHandler, { passive: true })
1143
+ }
1144
+
1145
+ function unbindScrollResize() {
1146
+ if (!session || !session.scrollResizeHandler) return
1147
+ document.removeEventListener('scroll', session.scrollResizeHandler, true)
1148
+ window.removeEventListener('resize', session.scrollResizeHandler)
1149
+ }
1150
+
1151
+ // ─── Pagination (1:1 with mentionjs _onDropdownScroll / _loadMoreResults) ─
1152
+
1153
+ function onDropdownScroll() {
1154
+ if (!session || !session.dropdown.el) return
1155
+ if (session.isLoadingMore || !session.nextPageUrl) return
1156
+ const { scrollTop, scrollHeight, clientHeight } = session.dropdown.el
1157
+ if (scrollTop + clientHeight >= scrollHeight - 10) loadMoreResults()
1158
+ }
1159
+
1160
+ function maybeLoadMore() {
1161
+ if (!session) return
1162
+ if (
1163
+ session.selectedIndex >= session.results.length - 2
1164
+ && session.nextPageUrl
1165
+ && !session.isLoadingMore
1166
+ ) {
1167
+ loadMoreResults()
1168
+ }
1169
+ }
1170
+
1171
+ async function loadMoreResults() {
1172
+ if (!session || session.isLoadingMore || !session.nextPageUrl) return
1173
+ session.isLoadingMore = true
1174
+ session.dropdown.showLoading()
1175
+ try {
1176
+ const prevLen = session.results.length
1177
+ const newItems = await runSearch(session.currentQuery, session.nextPageUrl)
1178
+ session?.dropdown.hideLoading()
1179
+ if (!session || newItems === null) return
1180
+ session.dropdown.appendItems(newItems, prevLen, session.selectedIndex)
1181
+ } catch (err) {
1182
+ // eslint-disable-next-line no-console
1183
+ console.warn('[mention-plugin] load more failed:', err)
1184
+ session?.dropdown.hideLoading()
1185
+ } finally {
1186
+ if (session) session.isLoadingMore = false
1187
+ }
1188
+ }
1189
+
1190
+ // ─── Session lifecycle ─────────────────────────────────────────────────
1191
+
1192
+ /**
1193
+ * Build a fresh dropdown configured with the current options/i18n.
1194
+ * @returns {DropdownUI}
1195
+ */
1196
+ function createDropdown() {
1197
+ return new DropdownUI({
1198
+ dropdownClass: opts.dropdownClass,
1199
+ renderItem: opts.renderItem,
1200
+ renderNoResults: opts.renderNoResults,
1201
+ renderLoading: opts.renderLoading,
1202
+ noResultsText: opts.noResultsText ?? t('noResults', 'No results found'),
1203
+ loadingText: t('loading', 'Loading...'),
1204
+ })
1205
+ }
1206
+
1207
+ /**
1208
+ * Attach the three document-level listeners shared by every session mode.
1209
+ * Must only be called when `session` is freshly set.
1210
+ */
1211
+ function attachSessionListeners() {
1212
+ if (!session) return
1213
+ // keydown on document (capture) so we fire before the editor's
1214
+ // TriggerManager/KeyboardManager on the editor root.
1215
+ document.addEventListener('keydown', session.keydownHandler, true)
1216
+ document.addEventListener('selectionchange', session.selectionChangeHandler)
1217
+ document.addEventListener('mousedown', session.outsideMouseDownHandler, true)
1218
+ }
1219
+
1220
+ /**
1221
+ * Create a `fresh` session — the user has just typed `@` in plain text.
1222
+ * @param {HTMLElement} parentEl
1223
+ * @param {Text} textNode
1224
+ * @param {number} triggerOffset
1225
+ * @param {InlinePluginContext} ctx
1226
+ */
1227
+ function openFreshSession(parentEl, textNode, triggerOffset, ctx) {
1228
+ session = {
1229
+ mode: 'fresh',
1230
+ parentEl,
1231
+ triggerTextNode: textNode,
1232
+ triggerOffset,
1233
+ span: null,
1234
+ dropdown: createDropdown(),
1235
+ keydownHandler: handleKeydown,
1236
+ selectionChangeHandler: handleSelectionChange,
1237
+ outsideMouseDownHandler: handleOutsideMouseDown,
1238
+ dropdownClickHandler: () => {},
1239
+ dropdownMouseDownHandler: () => {},
1240
+ dropdownScrollHandler: () => {},
1241
+ scrollResizeHandler: () => {},
1242
+ selectedIndex: 0,
1243
+ results: [],
1244
+ nextPageUrl: null,
1245
+ isLoadingMore: false,
1246
+ currentQuery: '',
1247
+ debounceTimer: null,
1248
+ debounceReject: null,
1249
+ searchController: null,
1250
+ searchRequestId: 0,
1251
+ ctx,
1252
+ committedInThisSession: false,
1253
+ ariaState: captureComboboxAria(parentEl),
1254
+ }
1255
+ attachSessionListeners()
1256
+ }
1257
+
1258
+ /**
1259
+ * Create an `edit` session — the user is mutating an already-committed
1260
+ * mention span (typing inside it, deleting chars, etc). The span enters
1261
+ * "editing" styling until the session closes.
1262
+ * @param {HTMLElement} span
1263
+ * @param {HTMLElement} parentEl
1264
+ * @param {InlinePluginContext} ctx
1265
+ */
1266
+ function openEditSession(span, parentEl, ctx) {
1267
+ session = {
1268
+ mode: 'edit',
1269
+ parentEl,
1270
+ triggerTextNode: null,
1271
+ triggerOffset: 0,
1272
+ span,
1273
+ dropdown: createDropdown(),
1274
+ keydownHandler: handleKeydown,
1275
+ selectionChangeHandler: handleSelectionChange,
1276
+ outsideMouseDownHandler: handleOutsideMouseDown,
1277
+ dropdownClickHandler: () => {},
1278
+ dropdownMouseDownHandler: () => {},
1279
+ dropdownScrollHandler: () => {},
1280
+ scrollResizeHandler: () => {},
1281
+ selectedIndex: 0,
1282
+ results: [],
1283
+ nextPageUrl: null,
1284
+ isLoadingMore: false,
1285
+ currentQuery: '',
1286
+ debounceTimer: null,
1287
+ debounceReject: null,
1288
+ searchController: null,
1289
+ searchRequestId: 0,
1290
+ ctx,
1291
+ committedInThisSession: false,
1292
+ ariaState: captureComboboxAria(parentEl),
1293
+ }
1294
+ span.classList.add('oe-ip--mention--editing')
1295
+ // Drop the stale `data-value` the moment edit mode starts — the user
1296
+ // is mutating the displayed name, so the id no longer matches what
1297
+ // they see. If a `ChangeNotifier` save fires mid-edit (250 ms debounce
1298
+ // default), the plugin's `isCommitted()` hook makes the shared inline
1299
+ // serializer store the visible query as plain text instead of emitting
1300
+ // an invalid widget entry. Commit restores the attribute.
1301
+ span.removeAttribute('data-value')
1302
+ attachSessionListeners()
1303
+ }
1304
+
1305
+ /**
1306
+ * If no session exists (or exists but for a different span), swap to a
1307
+ * fresh edit session anchored on `span`. Then kick off a search against
1308
+ * the current span text (minus the trigger) and refresh the dropdown.
1309
+ *
1310
+ * Called from `handleBeforeInput` whenever the user's edit inside a
1311
+ * committed pill changes the query.
1312
+ *
1313
+ * @param {HTMLElement} span
1314
+ * @param {HTMLElement} parentEl
1315
+ * @param {string} newQuery
1316
+ */
1317
+ function refreshEditSession(span, parentEl, newQuery) {
1318
+ if (!session || session.mode !== 'edit' || session.span !== span) {
1319
+ if (session) closeSession()
1320
+ const ctx = globalCtx
1321
+ if (!ctx) return // cannot proceed without an editor ctx
1322
+ openEditSession(span, parentEl, ctx)
1323
+ }
1324
+ runSearch(newQuery).then((items) => {
1325
+ if (items === null) return
1326
+ if (!session) return
1327
+ if (!inDOM(span)) { closeSession(); return }
1328
+ openOrUpdateDropdown(items)
1329
+ }).catch((err) => {
1330
+ // eslint-disable-next-line no-console
1331
+ console.warn('[mention-plugin] edit search failed:', err)
1332
+ })
1333
+ }
1334
+
1335
+ /**
1336
+ * Walk upwards from a node to find the containing block content element —
1337
+ * the nearest `contenteditable="true"` ancestor. Used by edit sessions
1338
+ * to locate the block the mention belongs to without relying on the
1339
+ * caller to pass it.
1340
+ * @param {Node} node
1341
+ * @returns {HTMLElement | null}
1342
+ */
1343
+ function findContenteditableBlock(node) {
1344
+ let cur = node.parentElement
1345
+ while (cur) {
1346
+ if (cur.isContentEditable || cur.getAttribute?.('contenteditable') === 'true') return cur
1347
+ cur = cur.parentElement
1348
+ }
1349
+ return null
1350
+ }
1351
+
1352
+ /**
1353
+ * Signal a content change to the editor.
1354
+ *
1355
+ * We dispatch a synthetic bubbling `input` event on the block — the
1356
+ * editor's `wireInputTracking` listens for this on its root element and
1357
+ * emits `EditorEvent.CHANGED`, which triggers `ChangeNotifier` → save.
1358
+ *
1359
+ * This is preferred over `globalCtx.notifyChanged()` because it doesn't
1360
+ * depend on having captured a ctx yet (safer when edit-mode mutations
1361
+ * fire from a pre-hydrated mention before any onEdit/hydrate call has
1362
+ * stashed the ctx).
1363
+ *
1364
+ * @param {HTMLElement | null} parentEl
1365
+ */
1366
+ function notifyChange(parentEl) {
1367
+ if (parentEl) {
1368
+ parentEl.dispatchEvent(new Event('input', { bubbles: true }))
1369
+ } else {
1370
+ globalCtx?.notifyChanged()
1371
+ }
1372
+ }
1373
+
1374
+ /**
1375
+ * Execute one mention edit as one editor history step.
1376
+ * Falls back to a synthetic input notification before the editor supplies
1377
+ * the mutation context.
1378
+ *
1379
+ * @param {HTMLElement} parentEl editable element that owns the mention
1380
+ * @param {() => void} operation synchronous DOM mutation
1381
+ * @returns {void}
1382
+ */
1383
+ function mutateContent(parentEl, operation) {
1384
+ if (globalCtx) {
1385
+ globalCtx.mutate(parentEl, operation)
1386
+ } else {
1387
+ operation()
1388
+ notifyChange(parentEl)
1389
+ }
1390
+ }
1391
+
1392
+ /**
1393
+ * Programmatically entering edit mode on a span we've stumbled into —
1394
+ * e.g. after `backspaceBeforeSpan` walks into a previous mention.
1395
+ * @param {HTMLElement} span
1396
+ */
1397
+ function openEditSessionForSpan(span) {
1398
+ const parentEl = findContenteditableBlock(span) || span.parentElement
1399
+ if (!parentEl) return
1400
+ const text = span.textContent || ''
1401
+ const query = text.startsWith(opts.trigger) ? text.slice(opts.trigger.length) : text
1402
+ refreshEditSession(span, parentEl, query)
1403
+ }
1404
+
1405
+ // ─── Before-input handling (port of mentionjs _handleInputInsideSpan) ──
1406
+
1407
+ /**
1408
+ * Intercepts `beforeinput` events when the caret is inside (or at the
1409
+ * edge of) a committed mention span. Drives all the char-by-char delete /
1410
+ * unwrap / re-search behavior — see the inline comments for per-case
1411
+ * logic. Mirrors mentionjs `_onBeforeInput` + `_handleInputInsideSpan`.
1412
+ *
1413
+ * @param {InputEvent} e
1414
+ */
1415
+ function handleBeforeInput(e) {
1416
+ const span = findMentionSpanAtCaret()
1417
+
1418
+ // Caret NOT in a mention span → defer to the editor's normal handling
1419
+ // for typing, deletes, etc. Fresh-trigger `@` detection is done by
1420
+ // TriggerManager (separate listener), not here.
1421
+ if (!span || !rootElement?.contains(span)) return
1422
+
1423
+ const sel = window.getSelection()
1424
+ if (!sel) return
1425
+
1426
+ // Guard: span detached (edge case during heavy editing).
1427
+ if (!inDOM(span)) {
1428
+ if (session?.mode === 'edit' && session.span === span) closeSession()
1429
+ return
1430
+ }
1431
+
1432
+ const parentEl = findContenteditableBlock(span) || span.parentElement
1433
+ if (!parentEl) return
1434
+
1435
+ const anchorNode = sel.anchorNode
1436
+ const anchorOffset = sel.anchorOffset
1437
+ const isEditing = session?.mode === 'edit' && session.span === span
1438
+
1439
+ const spanText = span.textContent || ''
1440
+ /** @type {number | null} */
1441
+ let cursorInText
1442
+ if (anchorNode && anchorNode.nodeType === Node.TEXT_NODE && anchorNode.parentElement === span) {
1443
+ cursorInText = anchorOffset
1444
+ } else if (anchorNode === span) {
1445
+ // Anchor is span element itself → treat offset 0 as start, 1 as end.
1446
+ cursorInText = anchorOffset === 0 ? 0 : spanText.length
1447
+ } else {
1448
+ cursorInText = null
1449
+ }
1450
+
1451
+ const isAtStart = cursorInText === 0
1452
+ const isAtEnd = cursorInText === spanText.length
1453
+
1454
+ // Case A — caret at start of span. Insert / delete happens OUTSIDE
1455
+ // the span (before it), treating the span as an opaque boundary.
1456
+ if (isAtStart) {
1457
+ e.preventDefault()
1458
+ if (isEditing) closeSession()
1459
+
1460
+ if (e.inputType === 'insertText' && typeof e.data === 'string') {
1461
+ mutateContent(parentEl, () => {
1462
+ const tn = document.createTextNode(e.data)
1463
+ span.parentNode?.insertBefore(tn, span)
1464
+ setCaretAt(tn, e.data.length)
1465
+ })
1466
+ } else if (e.inputType === 'deleteContentBackward') {
1467
+ mutateContent(parentEl, () => backspaceBeforeSpan(span))
1468
+ } else if (e.inputType === 'deleteContentForward') {
1469
+ mutateContent(parentEl, () => deleteForwardInSpan(span, spanText))
1470
+ }
1471
+ return
1472
+ }
1473
+
1474
+ // Case B — caret at end of a committed (non-editing) span. Typing
1475
+ // goes to a sibling text node AFTER the span (prevents growing the
1476
+ // pill with arbitrary user input).
1477
+ if (isAtEnd && !isEditing) {
1478
+ if ((e.inputType === 'insertText' || e.inputType === 'insertCompositionText') && typeof e.data === 'string') {
1479
+ e.preventDefault()
1480
+ mutateContent(parentEl, () => {
1481
+ const next = span.nextSibling
1482
+ if (next && next.nodeType === Node.TEXT_NODE) {
1483
+ next.textContent = e.data + (next.textContent || '')
1484
+ setCaretAt(next, e.data.length)
1485
+ } else {
1486
+ const char = e.data === ' ' ? '\u00A0' : e.data
1487
+ const tn = document.createTextNode(char)
1488
+ span.after(tn)
1489
+ setCaretAt(tn, 1)
1490
+ }
1491
+ })
1492
+ return
1493
+ }
1494
+ if (e.inputType === 'deleteContentForward') {
1495
+ e.preventDefault()
1496
+ mutateContent(parentEl, () => deleteForwardAfterNode(span))
1497
+ return
1498
+ }
1499
+ }
1500
+
1501
+ // Case C — Enter / line-break inside a span.
1502
+ if (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph') {
1503
+ // If actively editing with results → Enter commits via handleKeydown
1504
+ // (already bound); let the key handler run, just swallow the default.
1505
+ e.preventDefault()
1506
+ if (isEditing && session && session.results.length > 0) return
1507
+ // Otherwise: drop edit state (if any), place caret after span, insert <br>.
1508
+ // This matches mentionjs fallthrough; the editor's own Enter handling
1509
+ // (splitBlock) is intentionally bypassed inside a pill, because the
1510
+ // user was semantically editing the mention, not splitting the block.
1511
+ if (isEditing) closeSession()
1512
+ mutateContent(parentEl, () => {
1513
+ setCaretAfterNode(span)
1514
+ insertBr(window.getSelection() || sel)
1515
+ })
1516
+ return
1517
+ }
1518
+
1519
+ // Case D — Backspace inside the span (not at start).
1520
+ if (e.inputType === 'deleteContentBackward') {
1521
+ const offset = cursorInText ?? spanText.length
1522
+
1523
+ // Span contains ONLY the trigger → remove it entirely.
1524
+ if (spanText === opts.trigger) {
1525
+ e.preventDefault()
1526
+ if (isEditing) closeSessionForSpanReplacement(span)
1527
+ mutateContent(parentEl, () => {
1528
+ const prev = span.previousSibling
1529
+ const next = span.nextSibling
1530
+ span.parentNode?.removeChild(span)
1531
+ const range = document.createRange()
1532
+ if (prev && prev.nodeType === Node.TEXT_NODE) range.setStart(prev, (prev.textContent || '').length)
1533
+ else if (next) range.setStartBefore(next)
1534
+ else range.setStart(parentEl, 0)
1535
+ range.collapse(true)
1536
+ sel.removeAllRanges()
1537
+ sel.addRange(range)
1538
+ })
1539
+ return
1540
+ }
1541
+
1542
+ // Backspace immediately after the trigger deletes the trigger itself →
1543
+ // unwrap the span into plain text (minus the trigger).
1544
+ if (offset === opts.trigger.length && spanText.startsWith(opts.trigger)) {
1545
+ e.preventDefault()
1546
+ if (isEditing) closeSessionForSpanReplacement(span)
1547
+ mutateContent(parentEl, () => {
1548
+ const tn = document.createTextNode(spanText.substring(opts.trigger.length))
1549
+ span.parentNode?.insertBefore(tn, span)
1550
+ span.parentNode?.removeChild(span)
1551
+ setCaretAt(tn, 0)
1552
+ })
1553
+ return
1554
+ }
1555
+
1556
+ // Normal char deletion INSIDE the span. Shrink text, re-enter edit
1557
+ // mode, fire a fresh search.
1558
+ e.preventDefault()
1559
+ const newOffset = codePointStartBefore(spanText, offset)
1560
+ const newText = spanText.substring(0, newOffset) + spanText.substring(offset)
1561
+
1562
+ mutateContent(parentEl, () => {
1563
+ const tn = span.firstChild
1564
+ if (tn && tn.nodeType === Node.TEXT_NODE) tn.textContent = newText
1565
+ else span.textContent = newText
1566
+ setCaretAt(span.firstChild || span, newOffset)
1567
+ })
1568
+
1569
+ refreshEditSession(span, parentEl, newText.substring(opts.trigger.length))
1570
+ return
1571
+ }
1572
+
1573
+ // Case E — Forward-delete inside the span.
1574
+ if (e.inputType === 'deleteContentForward') {
1575
+ const offset = cursorInText ?? spanText.length
1576
+
1577
+ // At end → remove a char from the following sibling.
1578
+ if (offset >= spanText.length) {
1579
+ e.preventDefault()
1580
+ mutateContent(parentEl, () => deleteForwardAfterNode(span))
1581
+ return
1582
+ }
1583
+
1584
+ e.preventDefault()
1585
+ const newText = spanText.substring(0, offset) + spanText.substring(codePointEndAt(spanText, offset))
1586
+
1587
+ if (newText.length === 0) {
1588
+ if (isEditing) closeSessionForSpanReplacement(span)
1589
+ mutateContent(parentEl, () => {
1590
+ const prev = span.previousSibling
1591
+ const next = span.nextSibling
1592
+ span.parentNode?.removeChild(span)
1593
+ const range = document.createRange()
1594
+ if (next) range.setStartBefore(next)
1595
+ else if (prev && prev.nodeType === Node.TEXT_NODE) range.setStart(prev, (prev.textContent || '').length)
1596
+ else range.setStart(parentEl, 0)
1597
+ range.collapse(true)
1598
+ sel.removeAllRanges()
1599
+ sel.addRange(range)
1600
+ })
1601
+ return
1602
+ }
1603
+
1604
+ // If forward-delete ate the trigger → unwrap to plain text.
1605
+ if (offset === 0 && !newText.startsWith(opts.trigger)) {
1606
+ if (isEditing) closeSessionForSpanReplacement(span)
1607
+ mutateContent(parentEl, () => {
1608
+ const tn = document.createTextNode(newText)
1609
+ span.parentNode?.insertBefore(tn, span)
1610
+ span.parentNode?.removeChild(span)
1611
+ setCaretAt(tn, 0)
1612
+ })
1613
+ return
1614
+ }
1615
+
1616
+ mutateContent(parentEl, () => {
1617
+ const tn = span.firstChild
1618
+ if (tn && tn.nodeType === Node.TEXT_NODE) tn.textContent = newText
1619
+ else span.textContent = newText
1620
+ setCaretAt(span.firstChild || span, offset)
1621
+ })
1622
+
1623
+ refreshEditSession(span, parentEl, newText.substring(opts.trigger.length))
1624
+ return
1625
+ }
1626
+
1627
+ // Case F — Typing inside the span (not at start, not at end-with-committed).
1628
+ // Browser default inserts the char inside the span; we just re-run
1629
+ // the search afterwards.
1630
+ if ((e.inputType === 'insertText' || e.inputType === 'insertCompositionText') && typeof e.data === 'string') {
1631
+ const insertAt = cursorInText ?? spanText.length
1632
+ const newText = spanText.substring(0, insertAt) + e.data + spanText.substring(insertAt)
1633
+ refreshEditSession(span, parentEl, newText.substring(opts.trigger.length))
1634
+ // Don't preventDefault — let the browser do the actual DOM insertion.
1635
+ }
1636
+ }
1637
+
1638
+ // ─── Widget DOM (committed mention) ────────────────────────────────────
1639
+
1640
+ /**
1641
+ * Delegate widget DOM construction to the shared widget module so the
1642
+ * editor and document renderer use identical markup. The mention
1643
+ * widget is intentionally NOT `contentEditable="false"` — the caret can
1644
+ * enter it and Backspace / Delete / typing inside it work character-by-
1645
+ * character (matching the original `@shelamkoff/mentionjs` behavior).
1646
+ * `handleBeforeInput` keeps the span well-formed and re-opens the
1647
+ * suggestion dropdown when edits change the query.
1648
+ */
1649
+ const sharedWidget = createMentionWidget(opts.trigger)
1650
+ const createWidget = sharedWidget.createWidget
1651
+
1652
+ // ─── Plugin surface ─────────────────────────────────────────────────────
1653
+
1654
+ /** @type {InlinePlugin} */
1655
+ const plugin = {
1656
+ type: 'mention',
1657
+ trigger: opts.trigger,
1658
+ get title() { return t('title', 'Mention') },
1659
+
1660
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>',
1661
+
1662
+ /** @param {import('../../core/types').IScopedI18n} _i18n */
1663
+ setI18n(_i18n) { i18n = _i18n },
1664
+
1665
+ /**
1666
+ * Acquire resources only after the plugin belongs to a successfully
1667
+ * constructed editor. The listener is scoped to that editor root.
1668
+ * @param {HTMLElement} editorRoot
1669
+ * @param {InlinePluginContext} ctx
1670
+ */
1671
+ mount(editorRoot, ctx) {
1672
+ if (rootElement) throw new Error('Mention plugin is already mounted')
1673
+ rootElement = editorRoot
1674
+ globalCtx = ctx
1675
+ styleHandle = injectStyleUrls([STYLES_URL])
1676
+ rootElement.addEventListener('beforeinput', /** @type {EventListener} */ (handleBeforeInput), true)
1677
+ },
1678
+
1679
+ /**
1680
+ * Called by TriggerManager when `trigger` is typed at a word boundary
1681
+ * and again on every input while the session is active. `query` is the
1682
+ * text between the trigger character and the caret.
1683
+ *
1684
+ * @param {HTMLElement} parentEl
1685
+ * @param {string} query
1686
+ * @param {InlinePluginContext} ctx
1687
+ */
1688
+ onEdit(parentEl, query, ctx) {
1689
+ // Stash ctx for edit-mode mutations (fired via beforeinput outside of
1690
+ // a TriggerManager-driven flow — see `handleBeforeInput`).
1691
+ globalCtx = ctx
1692
+
1693
+ // Fresh trigger — close any stale session from a different block
1694
+ // before opening a new one. (Guards against TriggerManager edge cases
1695
+ // where onEdit fires with query='' while our session is still alive.)
1696
+ if (session && (query === '' || session.parentEl !== parentEl)) {
1697
+ closeSession()
1698
+ }
1699
+
1700
+ // Open a fresh session on first invocation.
1701
+ if (!session) {
1702
+ const sel = window.getSelection()
1703
+ if (!sel || !sel.anchorNode || sel.anchorNode.nodeType !== Node.TEXT_NODE) return
1704
+
1705
+ const textNode = /** @type {Text} */ (sel.anchorNode)
1706
+ const triggerOffset = sel.anchorOffset - opts.trigger.length
1707
+ if (triggerOffset < 0 || !textNode.data.startsWith(opts.trigger, triggerOffset)) return
1708
+
1709
+ openFreshSession(parentEl, textNode, triggerOffset, ctx)
1710
+ }
1711
+
1712
+ // Kick off / refresh search for the new query.
1713
+ // `onEdit` is invoked synchronously from the editor's TriggerManager;
1714
+ // we fire-and-forget here and update the dropdown when results arrive.
1715
+ runSearch(query).then((items) => {
1716
+ if (items === null) return // superseded / cancelled
1717
+ if (!session) return // session closed mid-request
1718
+ openOrUpdateDropdown(items)
1719
+ }).catch((err) => {
1720
+ // eslint-disable-next-line no-console
1721
+ console.warn('[mention-plugin] onEdit search failed:', err)
1722
+ })
1723
+ },
1724
+
1725
+ /** Close plugin-owned UI when the editor cancels the active trigger. */
1726
+ onCancel() { closeSession() },
1727
+
1728
+ /**
1729
+ * Build a committed widget. Called by `insertInlinePluginAtCaret` for
1730
+ * programmatic inserts and by the marshal pipeline for load-time
1731
+ * rehydration. Delegates to the shared widget factory; if `id` is
1732
+ * provided it's preserved as the span's `data-id` (marshal load path),
1733
+ * otherwise a fresh one is generated (programmatic fresh insert).
1734
+ *
1735
+ * @param {Record<string, string>} data
1736
+ * @param {string} [id]
1737
+ * @returns {HTMLElement}
1738
+ */
1739
+ createWidget(data, id) {
1740
+ return createWidget({ id: data.id || '', name: data.name || '' }, id)
1741
+ },
1742
+
1743
+ /**
1744
+ * Called by `hydrateInlinePlugins` for each committed widget in the DOM
1745
+ * when the editor loads saved content.
1746
+ *
1747
+ * The mention pill has no interactive controls (per spec — clickability
1748
+ * is a consumer concern) so there's nothing to wire up per widget. We
1749
+ * DO, however, capture the `InlinePluginContext` here so that the very
1750
+ * first edit action (e.g. user backspacing into a committed pill that
1751
+ * came from loaded content, before they ever typed `@` to trigger a
1752
+ * fresh session) can still call `notifyChanged()`.
1753
+ *
1754
+ * @param {HTMLElement} _element
1755
+ * @param {InlinePluginContext} ctx
1756
+ */
1757
+ hydrate(_element, ctx) { globalCtx = ctx },
1758
+
1759
+ /**
1760
+ * Delegate to the shared widget primitive — one source of truth for
1761
+ * how a mention span reads back into data, used by both the editor
1762
+ * marshal pipeline and document renderer.
1763
+ * @param {HTMLElement} element
1764
+ * @returns {Record<string, string>}
1765
+ */
1766
+ getData(element) {
1767
+ return /** @type {Record<string, string>} */ (sharedWidget.getData(element))
1768
+ },
1769
+
1770
+ /**
1771
+ * Exclude an in-progress edited pill from persistent widget data. Its
1772
+ * visible `@query` is retained as ordinary text by the shared serializer.
1773
+ * @param {HTMLElement} element
1774
+ * @returns {boolean}
1775
+ */
1776
+ isCommitted(element) {
1777
+ return !!element.dataset.value && !element.classList.contains('oe-ip--mention--editing')
1778
+ },
1779
+
1780
+ /**
1781
+ * Programmatic insertion via the toolbox `+` button.
1782
+ *
1783
+ * Per spec: behaves IDENTICALLY to the user typing the trigger
1784
+ * character manually — inserts just the trigger (`@`) at the caret,
1785
+ * positions the caret right after it, and opens a fresh session
1786
+ * (matching what `TriggerManager` would do if the `@` were typed).
1787
+ *
1788
+ * We can't rely on `TriggerManager` observing a programmatic DOM
1789
+ * mutation, so we dispatch a synthetic `input` event on the block —
1790
+ * `TriggerManager`'s `#onInput` listener then activates itself, sees
1791
+ * the freshly-inserted `@` at caret offset - 1, and calls our plugin's
1792
+ * `onEdit(parentEl, '', ctx)` naturally. From there everything flows
1793
+ * through the normal fresh-session code path.
1794
+ *
1795
+ * @param {InlinePluginContext} ctx
1796
+ */
1797
+ insertFresh(ctx) {
1798
+ globalCtx = ctx
1799
+
1800
+ // Resolve the caret's current block. Fall back to walking up from
1801
+ // `sel.anchorNode` — `setCaretToBlock` for an EMPTY paragraph only
1802
+ // calls `element.focus()` and doesn't set a selection range, so
1803
+ // `sel.rangeCount` may be 0. In that case we insert at the end of
1804
+ // whatever contenteditable is currently focused.
1805
+ const sel = window.getSelection()
1806
+ let parentEl = null
1807
+ if (sel && sel.rangeCount > 0) {
1808
+ const node = sel.anchorNode
1809
+ parentEl = node
1810
+ ? (node.nodeType === Node.ELEMENT_NODE
1811
+ ? (/** @type {HTMLElement} */ (node).isContentEditable
1812
+ ? /** @type {HTMLElement} */ (node)
1813
+ : findContenteditableBlock(node))
1814
+ : findContenteditableBlock(node))
1815
+ : null
1816
+ }
1817
+ if (!parentEl && document.activeElement instanceof HTMLElement && document.activeElement.isContentEditable) {
1818
+ parentEl = document.activeElement
1819
+ }
1820
+ if (!parentEl) return
1821
+
1822
+ // Chrome auto-inserts a `<br>` as the sole child of an otherwise
1823
+ // empty contenteditable element when it gains focus. That `<br>`
1824
+ // plays havoc with ranges (some specs want the range anchored INSIDE
1825
+ // it, which is invalid — it's a void element). Strip it so we have
1826
+ // a clean insertion point.
1827
+ if (
1828
+ parentEl.childNodes.length === 1
1829
+ && parentEl.firstChild instanceof HTMLElement
1830
+ && parentEl.firstChild.tagName === 'BR'
1831
+ ) {
1832
+ parentEl.removeChild(parentEl.firstChild)
1833
+ }
1834
+
1835
+ // Create the trigger text node and insert it at the current caret.
1836
+ // If we have a usable range inside parentEl, honor it; otherwise
1837
+ // fall back to appending at the end — matches "+" → type `@`
1838
+ // intent from an empty block.
1839
+ const tn = document.createTextNode(opts.trigger)
1840
+ let inserted = false
1841
+ if (sel && sel.rangeCount > 0) {
1842
+ const range = sel.getRangeAt(0)
1843
+ if (parentEl.contains(range.startContainer) || range.startContainer === parentEl) {
1844
+ try {
1845
+ range.deleteContents()
1846
+ range.insertNode(tn)
1847
+ inserted = true
1848
+ } catch {
1849
+ inserted = false
1850
+ }
1851
+ }
1852
+ }
1853
+ if (!inserted) {
1854
+ parentEl.appendChild(tn)
1855
+ }
1856
+
1857
+ // Caret directly after the inserted trigger.
1858
+ setCaretAt(tn, opts.trigger.length)
1859
+
1860
+ // Fire a synthetic `input` event so the editor's `TriggerManager`
1861
+ // (listening on `rootEl`) activates as if the user had typed `@`.
1862
+ // That listener reads the live DOM/selection — no dependency on
1863
+ // `event.isTrusted`. `wireInputTracking` also sees the input and
1864
+ // emits `CHANGED`, so there's no need to call `notifyChange` here.
1865
+ parentEl.dispatchEvent(new InputEvent('input', {
1866
+ bubbles: true,
1867
+ cancelable: false,
1868
+ inputType: 'insertText',
1869
+ data: opts.trigger,
1870
+ }))
1871
+ },
1872
+
1873
+ /**
1874
+ * Plugin-level cleanup hook. InlinePluginRegistry calls this once for the
1875
+ * owning editor and uses it to:
1876
+ * - close any open session;
1877
+ * - detach the editor-scoped `beforeinput` listener;
1878
+ * - release the shared stylesheet ref.
1879
+ *
1880
+ */
1881
+ destroy() {
1882
+ closeSession()
1883
+ rootElement?.removeEventListener('beforeinput', /** @type {EventListener} */ (handleBeforeInput), true)
1884
+ rootElement = null
1885
+ styleHandle?.destroy()
1886
+ styleHandle = null
1887
+ globalCtx = null
1888
+ },
1889
+ }
1890
+
1891
+ return plugin
1892
+ }
1893
+
1894
+ // Re-export the renderer widget factory so consumers can pick the
1895
+ // minimum surface they need (renderer-only uses `createMentionWidget`,
1896
+ // editor uses `createMentionPlugin`).
1897
+ export { createMentionWidget } from './widget.js'