@eightyfourthousand/lib-editing 2026.3.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 (304) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +7 -0
  4. package/jest.config.ts +10 -0
  5. package/package.json +35 -0
  6. package/postcss.config.mjs +9 -0
  7. package/project.json +29 -0
  8. package/src/fixtures/basic/json.ts +396 -0
  9. package/src/fixtures/toh251/json.ts +4913 -0
  10. package/src/fixtures/toh251/passages.ts +2814 -0
  11. package/src/fixtures/types.ts +8 -0
  12. package/src/index.ts +4 -0
  13. package/src/lib/block.ts +226 -0
  14. package/src/lib/components/editor/BlockEditor.tsx +38 -0
  15. package/src/lib/components/editor/EditorBackMatterPage.tsx +95 -0
  16. package/src/lib/components/editor/EditorBodyPage.tsx +87 -0
  17. package/src/lib/components/editor/EditorHeader.tsx +22 -0
  18. package/src/lib/components/editor/EditorLayout.tsx +62 -0
  19. package/src/lib/components/editor/EditorLeftPanelPage.tsx +27 -0
  20. package/src/lib/components/editor/EditorProvider.tsx +399 -0
  21. package/src/lib/components/editor/PaginationProvider.tsx +472 -0
  22. package/src/lib/components/editor/TitlesBuilder.tsx +40 -0
  23. package/src/lib/components/editor/TranslationBuilder.tsx +171 -0
  24. package/src/lib/components/editor/TranslationEditor.tsx +32 -0
  25. package/src/lib/components/editor/extensions/Abbreviation/Abbreviation.ts +133 -0
  26. package/src/lib/components/editor/extensions/Audio/Audio.ts +69 -0
  27. package/src/lib/components/editor/extensions/Bold.ts +43 -0
  28. package/src/lib/components/editor/extensions/Document.ts +8 -0
  29. package/src/lib/components/editor/extensions/DragHandle/DragHandle.ts +429 -0
  30. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLink.tsx +39 -0
  31. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkHoverContent.tsx +139 -0
  32. package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkMark.ts +236 -0
  33. package/src/lib/components/editor/extensions/EndNoteLink/endnote-utils.ts +412 -0
  34. package/src/lib/components/editor/extensions/GlobalConfig.ts +52 -0
  35. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInput.tsx +54 -0
  36. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstance.tsx +129 -0
  37. package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstanceNode.ts +148 -0
  38. package/src/lib/components/editor/extensions/Heading/Heading.ts +71 -0
  39. package/src/lib/components/editor/extensions/HoverInputField.tsx +54 -0
  40. package/src/lib/components/editor/extensions/Image.ts +18 -0
  41. package/src/lib/components/editor/extensions/Indent.ts +103 -0
  42. package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +173 -0
  43. package/src/lib/components/editor/extensions/InternalLink/InternalLinkHoverContent.tsx +137 -0
  44. package/src/lib/components/editor/extensions/InternalLink/InternalLinkInput.tsx +71 -0
  45. package/src/lib/components/editor/extensions/InternalLink/index.ts +1 -0
  46. package/src/lib/components/editor/extensions/Italic.ts +50 -0
  47. package/src/lib/components/editor/extensions/LeadingSpace.ts +106 -0
  48. package/src/lib/components/editor/extensions/Line/LineNode.ts +41 -0
  49. package/src/lib/components/editor/extensions/LineGroup/LineGroupNode.ts +124 -0
  50. package/src/lib/components/editor/extensions/Link/Link.ts +65 -0
  51. package/src/lib/components/editor/extensions/Link/LinkHoverContent.tsx +124 -0
  52. package/src/lib/components/editor/extensions/Link/index.ts +1 -0
  53. package/src/lib/components/editor/extensions/List.ts +74 -0
  54. package/src/lib/components/editor/extensions/Mantra/Mantra.ts +88 -0
  55. package/src/lib/components/editor/extensions/Mention/Mention.ts +184 -0
  56. package/src/lib/components/editor/extensions/Mention/MentionHoverContent.tsx +158 -0
  57. package/src/lib/components/editor/extensions/NodeWrapper.tsx +57 -0
  58. package/src/lib/components/editor/extensions/Paragraph/Paragraph.ts +25 -0
  59. package/src/lib/components/editor/extensions/ParagraphIndent.ts +87 -0
  60. package/src/lib/components/editor/extensions/Passage/EditLabel.tsx +57 -0
  61. package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +29 -0
  62. package/src/lib/components/editor/extensions/Passage/Passage.tsx +238 -0
  63. package/src/lib/components/editor/extensions/Passage/PassageNode.ts +223 -0
  64. package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +55 -0
  65. package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +92 -0
  66. package/src/lib/components/editor/extensions/Passage/index.ts +1 -0
  67. package/src/lib/components/editor/extensions/Passage/label.spec.ts +118 -0
  68. package/src/lib/components/editor/extensions/Passage/label.ts +39 -0
  69. package/src/lib/components/editor/extensions/Placeholder.ts +9 -0
  70. package/src/lib/components/editor/extensions/SlashCommand/SlashCommand.ts +65 -0
  71. package/src/lib/components/editor/extensions/SlashCommand/SuggestionList.tsx +109 -0
  72. package/src/lib/components/editor/extensions/SlashCommand/Suggestions.ts +185 -0
  73. package/src/lib/components/editor/extensions/SmallCaps.ts +110 -0
  74. package/src/lib/components/editor/extensions/StarterKit.ts +36 -0
  75. package/src/lib/components/editor/extensions/Subscript.ts +43 -0
  76. package/src/lib/components/editor/extensions/Superscript.ts +43 -0
  77. package/src/lib/components/editor/extensions/Table.ts +32 -0
  78. package/src/lib/components/editor/extensions/TextAlign.ts +5 -0
  79. package/src/lib/components/editor/extensions/TitleMetadata.ts +40 -0
  80. package/src/lib/components/editor/extensions/TitleNode.ts +133 -0
  81. package/src/lib/components/editor/extensions/TitlesNode.ts +102 -0
  82. package/src/lib/components/editor/extensions/Trailer.ts +57 -0
  83. package/src/lib/components/editor/extensions/TranslationDocument.ts +7 -0
  84. package/src/lib/components/editor/extensions/TranslationMetadata.ts +58 -0
  85. package/src/lib/components/editor/extensions/Underline.ts +43 -0
  86. package/src/lib/components/editor/hooks/index.ts +4 -0
  87. package/src/lib/components/editor/hooks/useBlockEditor.ts +53 -0
  88. package/src/lib/components/editor/hooks/useDefaultExtensions.ts +39 -0
  89. package/src/lib/components/editor/hooks/useDirtyStore.ts +33 -0
  90. package/src/lib/components/editor/hooks/useTranslationExtensions.ts +148 -0
  91. package/src/lib/components/editor/index.ts +10 -0
  92. package/src/lib/components/editor/menus/EmptyBubbleMenu.tsx +42 -0
  93. package/src/lib/components/editor/menus/MainBubbleMenu.tsx +51 -0
  94. package/src/lib/components/editor/menus/TranslationBubbleMenu.tsx +57 -0
  95. package/src/lib/components/editor/menus/index.ts +3 -0
  96. package/src/lib/components/editor/menus/selectors/EndNoteSelector.tsx +388 -0
  97. package/src/lib/components/editor/menus/selectors/GlossarySelector.tsx +63 -0
  98. package/src/lib/components/editor/menus/selectors/LinkSelector.tsx +68 -0
  99. package/src/lib/components/editor/menus/selectors/MantraSelector.tsx +119 -0
  100. package/src/lib/components/editor/menus/selectors/NodeSelector.tsx +144 -0
  101. package/src/lib/components/editor/menus/selectors/ParagraphButtons.tsx +68 -0
  102. package/src/lib/components/editor/menus/selectors/SelectorInputField.tsx +68 -0
  103. package/src/lib/components/editor/menus/selectors/TextAlignSelector.tsx +89 -0
  104. package/src/lib/components/editor/menus/selectors/TextButtons.tsx +89 -0
  105. package/src/lib/components/editor/menus/selectors/TranslationNodeSelector.tsx +143 -0
  106. package/src/lib/components/editor/menus/selectors/TranslationTextButtons.tsx +125 -0
  107. package/src/lib/components/editor/menus/selectors/index.ts +5 -0
  108. package/src/lib/components/editor/save-filter.spec.ts +94 -0
  109. package/src/lib/components/editor/save-filter.ts +27 -0
  110. package/src/lib/components/editor/util.ts +304 -0
  111. package/src/lib/components/index.ts +3 -0
  112. package/src/lib/components/reader/ReaderBackMatterPage.tsx +62 -0
  113. package/src/lib/components/reader/ReaderBackMatterPanel.tsx +53 -0
  114. package/src/lib/components/reader/ReaderBodyPage.tsx +46 -0
  115. package/src/lib/components/reader/ReaderBodyPanel.tsx +68 -0
  116. package/src/lib/components/reader/ReaderLayout.tsx +39 -0
  117. package/src/lib/components/reader/ReaderLeftPanel.tsx +8 -0
  118. package/src/lib/components/reader/ReaderLeftPanelPage.tsx +31 -0
  119. package/src/lib/components/reader/TranslationReader.tsx +28 -0
  120. package/src/lib/components/reader/index.ts +2 -0
  121. package/src/lib/components/reader/ssr.ts +3 -0
  122. package/src/lib/components/shared/AiSummarizerPage.tsx +12 -0
  123. package/src/lib/components/shared/BackMatterPanel.tsx +143 -0
  124. package/src/lib/components/shared/BodyPanel.tsx +214 -0
  125. package/src/lib/components/shared/HoverCardProvider.tsx +407 -0
  126. package/src/lib/components/shared/Imprint.tsx +24 -0
  127. package/src/lib/components/shared/LabeledElement.tsx +133 -0
  128. package/src/lib/components/shared/LeftPanel.tsx +65 -0
  129. package/src/lib/components/shared/NavigationContext.ts +64 -0
  130. package/src/lib/components/shared/NavigationProvider.tsx +368 -0
  131. package/src/lib/components/shared/OpenGraphImage.tsx +75 -0
  132. package/src/lib/components/shared/PassageSkeleton.tsx +10 -0
  133. package/src/lib/components/shared/RestrictionWarning.tsx +177 -0
  134. package/src/lib/components/shared/SourceReader.tsx +83 -0
  135. package/src/lib/components/shared/SuggestRevisionForm.tsx +99 -0
  136. package/src/lib/components/shared/TableOfContents.tsx +280 -0
  137. package/src/lib/components/shared/ThreeColumnRenderer.tsx +54 -0
  138. package/src/lib/components/shared/TranslationHeader.tsx +86 -0
  139. package/src/lib/components/shared/TranslationHoverCard.tsx +84 -0
  140. package/src/lib/components/shared/TranslationSkeleton.tsx +16 -0
  141. package/src/lib/components/shared/TranslationTable.tsx +155 -0
  142. package/src/lib/components/shared/bibliography/BibliographyBody.tsx +28 -0
  143. package/src/lib/components/shared/bibliography/BibliographyList.tsx +63 -0
  144. package/src/lib/components/shared/bibliography/index.ts +1 -0
  145. package/src/lib/components/shared/generate-metadata.ts +44 -0
  146. package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +144 -0
  147. package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +317 -0
  148. package/src/lib/components/shared/glossary/GlossarySkeleton.tsx +19 -0
  149. package/src/lib/components/shared/glossary/GlossaryTermList.tsx +58 -0
  150. package/src/lib/components/shared/glossary/index.ts +3 -0
  151. package/src/lib/components/shared/hooks/useGlossaryInstanceListener.tsx +42 -0
  152. package/src/lib/components/shared/hooks/useScrollInTab.tsx +43 -0
  153. package/src/lib/components/shared/hooks/useScrollPositionRestore.ts +274 -0
  154. package/src/lib/components/shared/hooks/useTohToggle.tsx +52 -0
  155. package/src/lib/components/shared/index.ts +11 -0
  156. package/src/lib/components/shared/ssr.ts +2 -0
  157. package/src/lib/components/shared/titles/FramedCard.tsx +132 -0
  158. package/src/lib/components/shared/titles/LongTitle.tsx +20 -0
  159. package/src/lib/components/shared/titles/LongTitles.tsx +28 -0
  160. package/src/lib/components/shared/titles/Title.tsx +54 -0
  161. package/src/lib/components/shared/titles/TitleDetails.tsx +47 -0
  162. package/src/lib/components/shared/titles/TitleForm.tsx +37 -0
  163. package/src/lib/components/shared/titles/Titles.tsx +114 -0
  164. package/src/lib/components/shared/titles/TitlesCard.tsx +113 -0
  165. package/src/lib/components/shared/titles/index.ts +8 -0
  166. package/src/lib/components/shared/types.ts +79 -0
  167. package/src/lib/components/ssr.ts +2 -0
  168. package/src/lib/exporters/abbreviation.spec.ts +31 -0
  169. package/src/lib/exporters/abbreviation.ts +22 -0
  170. package/src/lib/exporters/annotation.ts +193 -0
  171. package/src/lib/exporters/audio.spec.ts +77 -0
  172. package/src/lib/exporters/audio.ts +27 -0
  173. package/src/lib/exporters/blockquote.spec.ts +48 -0
  174. package/src/lib/exporters/blockquote.ts +24 -0
  175. package/src/lib/exporters/code.spec.ts +93 -0
  176. package/src/lib/exporters/code.ts +26 -0
  177. package/src/lib/exporters/end-note-link.spec.ts +104 -0
  178. package/src/lib/exporters/end-note-link.ts +35 -0
  179. package/src/lib/exporters/export.ts +12 -0
  180. package/src/lib/exporters/glossary-instance.spec.ts +85 -0
  181. package/src/lib/exporters/glossary-instance.ts +31 -0
  182. package/src/lib/exporters/has-abbreviation.spec.ts +31 -0
  183. package/src/lib/exporters/has-abbreviation.ts +21 -0
  184. package/src/lib/exporters/heading.spec.ts +80 -0
  185. package/src/lib/exporters/heading.ts +28 -0
  186. package/src/lib/exporters/image.spec.ts +48 -0
  187. package/src/lib/exporters/image.ts +25 -0
  188. package/src/lib/exporters/indent.spec.ts +58 -0
  189. package/src/lib/exporters/indent.ts +18 -0
  190. package/src/lib/exporters/index.ts +1 -0
  191. package/src/lib/exporters/internal-link.spec.ts +90 -0
  192. package/src/lib/exporters/internal-link.ts +35 -0
  193. package/src/lib/exporters/italic.spec.ts +84 -0
  194. package/src/lib/exporters/italic.ts +55 -0
  195. package/src/lib/exporters/leading-space.spec.ts +28 -0
  196. package/src/lib/exporters/leading-space.ts +16 -0
  197. package/src/lib/exporters/line-group.spec.ts +48 -0
  198. package/src/lib/exporters/line-group.ts +24 -0
  199. package/src/lib/exporters/line.spec.ts +48 -0
  200. package/src/lib/exporters/line.ts +24 -0
  201. package/src/lib/exporters/link.spec.ts +123 -0
  202. package/src/lib/exporters/link.ts +67 -0
  203. package/src/lib/exporters/list-item.spec.ts +48 -0
  204. package/src/lib/exporters/list-item.ts +24 -0
  205. package/src/lib/exporters/list.spec.ts +82 -0
  206. package/src/lib/exporters/list.ts +31 -0
  207. package/src/lib/exporters/mantra.spec.ts +51 -0
  208. package/src/lib/exporters/mantra.ts +25 -0
  209. package/src/lib/exporters/mention.ts +41 -0
  210. package/src/lib/exporters/paragraph.spec.ts +173 -0
  211. package/src/lib/exporters/paragraph.ts +32 -0
  212. package/src/lib/exporters/quote.spec.ts +56 -0
  213. package/src/lib/exporters/quote.ts +25 -0
  214. package/src/lib/exporters/span.spec.ts +118 -0
  215. package/src/lib/exporters/span.ts +44 -0
  216. package/src/lib/exporters/table-body-data.spec.ts +48 -0
  217. package/src/lib/exporters/table-body-data.ts +24 -0
  218. package/src/lib/exporters/table-body-header.spec.ts +48 -0
  219. package/src/lib/exporters/table-body-header.ts +24 -0
  220. package/src/lib/exporters/table-body-row.spec.ts +48 -0
  221. package/src/lib/exporters/table-body-row.ts +24 -0
  222. package/src/lib/exporters/table.spec.ts +48 -0
  223. package/src/lib/exporters/table.ts +24 -0
  224. package/src/lib/exporters/trailer.spec.ts +48 -0
  225. package/src/lib/exporters/trailer.ts +24 -0
  226. package/src/lib/exporters/util.ts +62 -0
  227. package/src/lib/passage.ts +182 -0
  228. package/src/lib/titles.ts +80 -0
  229. package/src/lib/transformers/abbreviation.spec.ts +87 -0
  230. package/src/lib/transformers/abbreviation.ts +30 -0
  231. package/src/lib/transformers/annotate.ts +146 -0
  232. package/src/lib/transformers/audio.spec.ts +55 -0
  233. package/src/lib/transformers/audio.ts +29 -0
  234. package/src/lib/transformers/blockquote.spec.ts +48 -0
  235. package/src/lib/transformers/blockquote.ts +41 -0
  236. package/src/lib/transformers/code.spec.ts +52 -0
  237. package/src/lib/transformers/code.ts +22 -0
  238. package/src/lib/transformers/deprecated.ts +7 -0
  239. package/src/lib/transformers/end-note-link.spec.ts +56 -0
  240. package/src/lib/transformers/end-note-link.ts +76 -0
  241. package/src/lib/transformers/glossary-instance.spec.ts +55 -0
  242. package/src/lib/transformers/glossary-instance.ts +40 -0
  243. package/src/lib/transformers/has-abbreviation.spec.ts +50 -0
  244. package/src/lib/transformers/has-abbreviation.ts +30 -0
  245. package/src/lib/transformers/heading.spec.ts +62 -0
  246. package/src/lib/transformers/heading.ts +30 -0
  247. package/src/lib/transformers/image.spec.ts +51 -0
  248. package/src/lib/transformers/image.ts +28 -0
  249. package/src/lib/transformers/indent.spec.ts +53 -0
  250. package/src/lib/transformers/indent.ts +17 -0
  251. package/src/lib/transformers/index.ts +33 -0
  252. package/src/lib/transformers/inline-title.spec.ts +59 -0
  253. package/src/lib/transformers/inline-title.ts +34 -0
  254. package/src/lib/transformers/internal-link.spec.ts +67 -0
  255. package/src/lib/transformers/internal-link.ts +65 -0
  256. package/src/lib/transformers/italic.ts +22 -0
  257. package/src/lib/transformers/leading-space.spec.ts +55 -0
  258. package/src/lib/transformers/leading-space.ts +17 -0
  259. package/src/lib/transformers/line-group.spec.ts +48 -0
  260. package/src/lib/transformers/line-group.ts +37 -0
  261. package/src/lib/transformers/line.spec.ts +54 -0
  262. package/src/lib/transformers/line.ts +27 -0
  263. package/src/lib/transformers/link.spec.ts +61 -0
  264. package/src/lib/transformers/link.ts +27 -0
  265. package/src/lib/transformers/list-item.spec.ts +48 -0
  266. package/src/lib/transformers/list-item.ts +37 -0
  267. package/src/lib/transformers/list.spec.ts +58 -0
  268. package/src/lib/transformers/list.ts +42 -0
  269. package/src/lib/transformers/mantra.spec.ts +58 -0
  270. package/src/lib/transformers/mantra.ts +28 -0
  271. package/src/lib/transformers/mention.ts +70 -0
  272. package/src/lib/transformers/paragraph.spec.ts +46 -0
  273. package/src/lib/transformers/paragraph.ts +26 -0
  274. package/src/lib/transformers/quote.spec.ts +42 -0
  275. package/src/lib/transformers/quote.ts +3 -0
  276. package/src/lib/transformers/quoted.ts +3 -0
  277. package/src/lib/transformers/recurse.ts +76 -0
  278. package/src/lib/transformers/reference.ts +3 -0
  279. package/src/lib/transformers/span.spec.ts +68 -0
  280. package/src/lib/transformers/span.ts +78 -0
  281. package/src/lib/transformers/split-at.ts +58 -0
  282. package/src/lib/transformers/split-block.ts +110 -0
  283. package/src/lib/transformers/split-content.ts +67 -0
  284. package/src/lib/transformers/split-insert.ts +76 -0
  285. package/src/lib/transformers/split-marks.ts +42 -0
  286. package/src/lib/transformers/split-node.ts +138 -0
  287. package/src/lib/transformers/table-body-data.spec.ts +44 -0
  288. package/src/lib/transformers/table-body-data.ts +29 -0
  289. package/src/lib/transformers/table-body-header.spec.ts +44 -0
  290. package/src/lib/transformers/table-body-header.ts +29 -0
  291. package/src/lib/transformers/table-body-row.spec.ts +44 -0
  292. package/src/lib/transformers/table-body-row.ts +29 -0
  293. package/src/lib/transformers/table.spec.ts +47 -0
  294. package/src/lib/transformers/table.ts +29 -0
  295. package/src/lib/transformers/trailer.spec.ts +43 -0
  296. package/src/lib/transformers/trailer.ts +26 -0
  297. package/src/lib/transformers/transformer.ts +25 -0
  298. package/src/lib/transformers/unknown.ts +8 -0
  299. package/src/lib/transformers/util.ts +20 -0
  300. package/src/lib/types.ts +10 -0
  301. package/src/ssr.ts +1 -0
  302. package/tsconfig.json +20 -0
  303. package/tsconfig.lib.json +29 -0
  304. package/tsconfig.spec.json +22 -0
@@ -0,0 +1,399 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useCallback, useContext, useRef } from 'react';
4
+ import { Editor } from '@tiptap/react';
5
+ import { Doc, Transaction, XmlElement, XmlFragment, YEvent } from 'yjs';
6
+ import type { Passage, Work } from '@eightyfourthousand/data-access';
7
+ import {
8
+ createGraphQLClient,
9
+ hasPermission,
10
+ savePassages,
11
+ } from '@eightyfourthousand/client-graphql';
12
+ import { passagesFromNodes, ensureUuids } from '../../passage';
13
+ import { NavigationProvider } from '../shared';
14
+ import { useDirtyStore, type DirtyStore } from './hooks/useDirtyStore';
15
+ import { computeSavePayload } from './save-filter';
16
+
17
+ interface EditorContextState {
18
+ doc?: Doc;
19
+ work: Work;
20
+ dirtyStore: DirtyStore;
21
+ canEdit(): Promise<boolean>;
22
+ getFragment: (builder: string) => XmlFragment;
23
+ setDoc: (doc: Doc) => void;
24
+ getEditor: (key: string) => Editor | undefined;
25
+ setEditor: (key: string, editor?: Editor) => void;
26
+ save: () => Promise<void>;
27
+ startObserving: (builder: string) => void;
28
+ stopObserving: (builder: string) => void;
29
+ setNavigating: (
30
+ navigating: boolean,
31
+ resetKnownUuids?: boolean,
32
+ fragment?: XmlFragment,
33
+ ) => void;
34
+ isNavigating: () => boolean;
35
+ }
36
+
37
+ export const EditorContext = createContext<EditorContextState>({
38
+ work: {
39
+ uuid: '',
40
+ title: '',
41
+ section: '',
42
+ pages: 0,
43
+ publicationDate: new Date(),
44
+ publicationVersion: '0.0.0',
45
+ restriction: false,
46
+ toh: [],
47
+ },
48
+ dirtyStore: {
49
+ isDirty: false,
50
+ listeners: new Set(),
51
+ subscribe: () => () => {
52
+ // No-op cleanup - safe for useSyncExternalStore in reader mode
53
+ },
54
+ setDirty: () => {
55
+ // No-op when outside provider (reader mode)
56
+ },
57
+ getSnapshot: () => false,
58
+ },
59
+ canEdit: async () => false,
60
+ getFragment: () => {
61
+ throw Error('Not implemented');
62
+ },
63
+ setDoc: () => {
64
+ throw Error('Not implemented');
65
+ },
66
+ getEditor: () => undefined,
67
+ setEditor: () => {
68
+ throw Error('Not implemented');
69
+ },
70
+ save: async () => {
71
+ // No-op when outside provider
72
+ },
73
+ startObserving: () => {
74
+ throw Error('Not implemented');
75
+ },
76
+ stopObserving: () => {
77
+ throw Error('Not implemented');
78
+ },
79
+ setNavigating: () => {
80
+ // No-op when outside provider
81
+ },
82
+ isNavigating: () => false,
83
+ });
84
+
85
+ interface EditorContextProps {
86
+ work: Work;
87
+ doc?: Doc;
88
+ children: React.ReactNode;
89
+ }
90
+
91
+ export const EditorContextProvider = ({
92
+ work,
93
+ doc: initialDoc,
94
+ children,
95
+ }: EditorContextProps) => {
96
+ const client = createGraphQLClient();
97
+
98
+ const [doc, setDoc] = React.useState<Doc>(initialDoc || new Doc());
99
+ // Use ref for immediate tracking to avoid state updates on every keystroke
100
+ const dirtyUuidsRef = useRef<Set<string>>(new Set());
101
+ const deletedUuidsRef = useRef<Set<string>>(new Set());
102
+ const isNavigatingRef = useRef(false);
103
+ const clearedFragmentRef = useRef<XmlFragment | null>(null);
104
+ const knownUuidsRef = useRef<Map<XmlFragment, Set<string>>>(new Map());
105
+
106
+ // Store for dirty state with subscription support
107
+ const dirtyStore = useDirtyStore();
108
+
109
+ const editorCache = useRef<{ [key: string]: Editor }>({});
110
+
111
+ const getEditor = useCallback((key: string) => {
112
+ return editorCache.current[key];
113
+ }, []);
114
+
115
+ const setEditor = useCallback((key: string, editor?: Editor) => {
116
+ if (!editor && editorCache.current[key]) {
117
+ delete editorCache.current[key];
118
+ } else if (editor) {
119
+ editorCache.current[key] = editor;
120
+ }
121
+ }, []);
122
+
123
+ const getFragment = useCallback(
124
+ (builder: string): XmlFragment => {
125
+ return doc?.getXmlFragment(builder);
126
+ },
127
+ [doc],
128
+ );
129
+
130
+ const save = useCallback(async () => {
131
+ const editors = Object.values(editorCache.current);
132
+ if (!editors.length) {
133
+ console.warn('No editor instance found, cannot save.');
134
+ return;
135
+ }
136
+
137
+ // Use the ref directly for the most up-to-date dirty/deleted UUIDs
138
+ const {
139
+ uuidsToSave,
140
+ uuidsToDelete: deletedUuids,
141
+ hasChanges,
142
+ } = computeSavePayload({
143
+ dirtyUuids: dirtyUuidsRef.current,
144
+ deletedUuids: deletedUuidsRef.current,
145
+ });
146
+
147
+ if (!hasChanges) {
148
+ console.log('No changes to save.');
149
+ return;
150
+ }
151
+
152
+ const passages: Passage[] = [];
153
+ if (uuidsToSave.length) {
154
+ editors.forEach((editor) => {
155
+ // Skip editors whose DOM view has been unmounted (e.g. inactive tabs).
156
+ // Calling blur()/focus() on a destroyed editor throws a TipTap error
157
+ // about view['hasFocus'] not being accessible.
158
+ if (editor.isDestroyed) return;
159
+ // Ensure all nodes have unique, non-null UUIDs before reading them.
160
+ // New paragraph nodes created by splitting (e.g. pressing Enter) have
161
+ // uuid: null until the NodeView mount cycle runs validateAttrs. If we
162
+ // read the document before that cycle completes, annotationExportsFromNode
163
+ // silently drops any annotation whose node.attrs.uuid is falsy, producing
164
+ // missing paragraph annotations. ensureUuids fixes this synchronously.
165
+ ensureUuids(editor);
166
+ editor.commands.blur();
167
+ passages.push(
168
+ ...passagesFromNodes({
169
+ uuids: uuidsToSave,
170
+ workUuid: work.uuid,
171
+ editor,
172
+ }),
173
+ );
174
+ editor.commands.focus();
175
+ });
176
+ }
177
+ const result = await savePassages({
178
+ client,
179
+ passages,
180
+ deletedUuids: deletedUuids.length > 0 ? deletedUuids : undefined,
181
+ });
182
+
183
+ if (!result?.success) {
184
+ console.error('Save failed:', result?.error ?? 'unknown error');
185
+ return;
186
+ }
187
+
188
+ console.log('Document state saved.');
189
+
190
+ // Clear both ref and state
191
+ dirtyUuidsRef.current.clear();
192
+ deletedUuidsRef.current.clear();
193
+ dirtyStore.setDirty(false);
194
+ }, [editorCache, client, work.uuid]);
195
+
196
+ const observerFunction = useCallback(
197
+ (evts: YEvent<XmlFragment | XmlElement>[], txn: Transaction) => {
198
+ if (!txn.local) {
199
+ return;
200
+ }
201
+
202
+ // Detect passage deletions by diffing known UUIDs against current fragment state.
203
+ // We walk up from any event target to find the observed XmlFragment rather than
204
+ // checking each event's target, because merge operations may only produce events
205
+ // targeting passage-level XmlElements (not the fragment itself).
206
+ // We intentionally avoid using Yjs `changes.deleted` because operations like
207
+ // splitPassage (replaceWith + insert) cause the binding to delete and re-create
208
+ // XmlElements internally, producing false positives.
209
+ if (!isNavigatingRef.current && evts.length > 0) {
210
+ let fragment: XmlFragment | null = null;
211
+ let current: XmlFragment | XmlElement | null = evts[0].target;
212
+ while (current) {
213
+ if (
214
+ current instanceof XmlFragment &&
215
+ !(current instanceof XmlElement)
216
+ ) {
217
+ fragment = current;
218
+ break;
219
+ }
220
+ current = current.parent as XmlFragment | XmlElement | null;
221
+ }
222
+
223
+ if (fragment) {
224
+ const currentUuids = new Set<string>();
225
+ for (let i = 0; i < fragment.length; i++) {
226
+ const child = fragment.get(i);
227
+ if (child instanceof XmlElement && child.nodeName === 'passage') {
228
+ const uuid = child.getAttribute('uuid');
229
+ if (uuid) {
230
+ currentUuids.add(uuid);
231
+ }
232
+ }
233
+ }
234
+
235
+ const knownForFragment = knownUuidsRef.current.get(fragment);
236
+ if (knownForFragment && knownForFragment.size > 0) {
237
+ // Detect deletions: UUIDs in known set but not in current
238
+ knownForFragment.forEach((uuid) => {
239
+ if (!currentUuids.has(uuid)) {
240
+ deletedUuidsRef.current.add(uuid);
241
+ }
242
+ });
243
+
244
+ // Detect new passages: UUIDs in current but not in known.
245
+ // This catches passages created by splitPassage, where the Yjs
246
+ // binding creates XmlElements with attributes set during construction
247
+ // (pre-integration), so txn.changed never includes them.
248
+ currentUuids.forEach((uuid) => {
249
+ if (!knownForFragment.has(uuid)) {
250
+ dirtyUuidsRef.current.add(uuid);
251
+ }
252
+ });
253
+ }
254
+
255
+ knownUuidsRef.current.set(fragment, currentUuids);
256
+ }
257
+ }
258
+
259
+ if (!isNavigatingRef.current) {
260
+ txn.changed.forEach((_change, key) => {
261
+ // Start from key itself (not key.parent) so that structural changes
262
+ // directly on a passage XmlElement (e.g. children moved during merge)
263
+ // are detected. For leaf types like XmlText, nodeName is undefined so
264
+ // the walk-up proceeds to the parent as before.
265
+ let node = key as unknown as XmlElement;
266
+ while (node?.nodeName !== 'passage' && node?.parent) {
267
+ node = node.parent as XmlElement;
268
+ }
269
+
270
+ const uuid = node?.getAttribute?.('uuid');
271
+ if (uuid) {
272
+ // Add directly to ref - no state update on every keystroke
273
+ dirtyUuidsRef.current.add(uuid);
274
+ // Only update dirty state if it's not already dirty
275
+ // This prevents re-renders on every keystroke after the first one
276
+ if (!dirtyStore.isDirty) {
277
+ dirtyStore.setDirty(true);
278
+ }
279
+ }
280
+ });
281
+ }
282
+
283
+ // Mark dirty if there are deletions
284
+ if (deletedUuidsRef.current.size > 0 && !dirtyStore.isDirty) {
285
+ dirtyStore.setDirty(true);
286
+ }
287
+ },
288
+ [],
289
+ );
290
+
291
+ const startObserving = useCallback(
292
+ (builder: string) => {
293
+ const fragment = getFragment(builder);
294
+ if (fragment) {
295
+ // Pre-populate knownUuidsRef so the diff-based deletion detection
296
+ // has a baseline from the very first observer event. Without this,
297
+ // a merge that happens before any other edit would not be detected
298
+ // because knownUuidsRef would be empty and the diff would be skipped.
299
+ const uuids = new Set<string>();
300
+ for (let i = 0; i < fragment.length; i++) {
301
+ const child = fragment.get(i);
302
+ if (child instanceof XmlElement && child.nodeName === 'passage') {
303
+ const uuid = child.getAttribute('uuid');
304
+ if (uuid) {
305
+ uuids.add(uuid);
306
+ }
307
+ }
308
+ }
309
+ knownUuidsRef.current.set(fragment, uuids);
310
+
311
+ fragment.observeDeep(observerFunction);
312
+ }
313
+ },
314
+ [getFragment, observerFunction],
315
+ );
316
+
317
+ const stopObserving = useCallback(
318
+ (builder: string) => {
319
+ const fragment = getFragment(builder);
320
+ if (fragment && observerFunction) {
321
+ fragment.unobserveDeep(observerFunction);
322
+ }
323
+ },
324
+ [getFragment, observerFunction],
325
+ );
326
+
327
+ const setNavigating = useCallback(
328
+ (
329
+ navigating: boolean,
330
+ resetKnownUuids = false,
331
+ fragment?: XmlFragment,
332
+ ) => {
333
+ isNavigatingRef.current = navigating;
334
+ if (navigating && resetKnownUuids) {
335
+ // Clear known UUIDs so the first observer call after navigation
336
+ // repopulates without diffing against the stale pre-navigation set.
337
+ // Only done for full navigation (clearContent + setContent), not for
338
+ // load-more which only adds passages and needs to keep its baseline.
339
+ // When a specific fragment is provided, only clear that fragment's
340
+ // entry so other editors (e.g. endnotes) retain their baseline and
341
+ // can still detect deletions.
342
+ if (fragment) {
343
+ knownUuidsRef.current.delete(fragment);
344
+ clearedFragmentRef.current = fragment;
345
+ } else {
346
+ knownUuidsRef.current.clear();
347
+ }
348
+ }
349
+
350
+ // When navigation ends, repopulate the baseline for any fragment that
351
+ // was cleared so deletion detection works immediately — without waiting
352
+ // for an intervening Y.js event to re-establish the baseline.
353
+ if (!navigating && clearedFragmentRef.current) {
354
+ const f = clearedFragmentRef.current;
355
+ clearedFragmentRef.current = null;
356
+ const uuids = new Set<string>();
357
+ for (let i = 0; i < f.length; i++) {
358
+ const child = f.get(i);
359
+ if (child instanceof XmlElement && child.nodeName === 'passage') {
360
+ const uuid = child.getAttribute('uuid');
361
+ if (uuid) uuids.add(uuid);
362
+ }
363
+ }
364
+ knownUuidsRef.current.set(f, uuids);
365
+ }
366
+ },
367
+ [],
368
+ );
369
+
370
+ const isNavigating = useCallback(() => isNavigatingRef.current, []);
371
+
372
+ const canEdit = useCallback(async () => {
373
+ return await hasPermission({ client, permission: 'EDITOR_EDIT' });
374
+ }, [client]);
375
+
376
+ return (
377
+ <EditorContext.Provider
378
+ value={{
379
+ work,
380
+ doc,
381
+ dirtyStore,
382
+ canEdit,
383
+ getFragment,
384
+ setDoc,
385
+ getEditor,
386
+ setEditor,
387
+ save,
388
+ startObserving,
389
+ stopObserving,
390
+ setNavigating,
391
+ isNavigating,
392
+ }}
393
+ >
394
+ <NavigationProvider uuid={work.uuid}>{children}</NavigationProvider>
395
+ </EditorContext.Provider>
396
+ );
397
+ };
398
+
399
+ export const useEditorState = () => useContext(EditorContext);