@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.
- package/.babelrc +12 -0
- package/.eslintrc.json +18 -0
- package/README.md +7 -0
- package/jest.config.ts +10 -0
- package/package.json +35 -0
- package/postcss.config.mjs +9 -0
- package/project.json +29 -0
- package/src/fixtures/basic/json.ts +396 -0
- package/src/fixtures/toh251/json.ts +4913 -0
- package/src/fixtures/toh251/passages.ts +2814 -0
- package/src/fixtures/types.ts +8 -0
- package/src/index.ts +4 -0
- package/src/lib/block.ts +226 -0
- package/src/lib/components/editor/BlockEditor.tsx +38 -0
- package/src/lib/components/editor/EditorBackMatterPage.tsx +95 -0
- package/src/lib/components/editor/EditorBodyPage.tsx +87 -0
- package/src/lib/components/editor/EditorHeader.tsx +22 -0
- package/src/lib/components/editor/EditorLayout.tsx +62 -0
- package/src/lib/components/editor/EditorLeftPanelPage.tsx +27 -0
- package/src/lib/components/editor/EditorProvider.tsx +399 -0
- package/src/lib/components/editor/PaginationProvider.tsx +472 -0
- package/src/lib/components/editor/TitlesBuilder.tsx +40 -0
- package/src/lib/components/editor/TranslationBuilder.tsx +171 -0
- package/src/lib/components/editor/TranslationEditor.tsx +32 -0
- package/src/lib/components/editor/extensions/Abbreviation/Abbreviation.ts +133 -0
- package/src/lib/components/editor/extensions/Audio/Audio.ts +69 -0
- package/src/lib/components/editor/extensions/Bold.ts +43 -0
- package/src/lib/components/editor/extensions/Document.ts +8 -0
- package/src/lib/components/editor/extensions/DragHandle/DragHandle.ts +429 -0
- package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLink.tsx +39 -0
- package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkHoverContent.tsx +139 -0
- package/src/lib/components/editor/extensions/EndNoteLink/EndNoteLinkMark.ts +236 -0
- package/src/lib/components/editor/extensions/EndNoteLink/endnote-utils.ts +412 -0
- package/src/lib/components/editor/extensions/GlobalConfig.ts +52 -0
- package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInput.tsx +54 -0
- package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstance.tsx +129 -0
- package/src/lib/components/editor/extensions/GlossaryInstance/GlossaryInstanceNode.ts +148 -0
- package/src/lib/components/editor/extensions/Heading/Heading.ts +71 -0
- package/src/lib/components/editor/extensions/HoverInputField.tsx +54 -0
- package/src/lib/components/editor/extensions/Image.ts +18 -0
- package/src/lib/components/editor/extensions/Indent.ts +103 -0
- package/src/lib/components/editor/extensions/InternalLink/InternalLink.ts +173 -0
- package/src/lib/components/editor/extensions/InternalLink/InternalLinkHoverContent.tsx +137 -0
- package/src/lib/components/editor/extensions/InternalLink/InternalLinkInput.tsx +71 -0
- package/src/lib/components/editor/extensions/InternalLink/index.ts +1 -0
- package/src/lib/components/editor/extensions/Italic.ts +50 -0
- package/src/lib/components/editor/extensions/LeadingSpace.ts +106 -0
- package/src/lib/components/editor/extensions/Line/LineNode.ts +41 -0
- package/src/lib/components/editor/extensions/LineGroup/LineGroupNode.ts +124 -0
- package/src/lib/components/editor/extensions/Link/Link.ts +65 -0
- package/src/lib/components/editor/extensions/Link/LinkHoverContent.tsx +124 -0
- package/src/lib/components/editor/extensions/Link/index.ts +1 -0
- package/src/lib/components/editor/extensions/List.ts +74 -0
- package/src/lib/components/editor/extensions/Mantra/Mantra.ts +88 -0
- package/src/lib/components/editor/extensions/Mention/Mention.ts +184 -0
- package/src/lib/components/editor/extensions/Mention/MentionHoverContent.tsx +158 -0
- package/src/lib/components/editor/extensions/NodeWrapper.tsx +57 -0
- package/src/lib/components/editor/extensions/Paragraph/Paragraph.ts +25 -0
- package/src/lib/components/editor/extensions/ParagraphIndent.ts +87 -0
- package/src/lib/components/editor/extensions/Passage/EditLabel.tsx +57 -0
- package/src/lib/components/editor/extensions/Passage/EditorOptions.tsx +29 -0
- package/src/lib/components/editor/extensions/Passage/Passage.tsx +238 -0
- package/src/lib/components/editor/extensions/Passage/PassageNode.ts +223 -0
- package/src/lib/components/editor/extensions/Passage/ReaderOptions.tsx +55 -0
- package/src/lib/components/editor/extensions/Passage/ShowAnnotations.tsx +92 -0
- package/src/lib/components/editor/extensions/Passage/index.ts +1 -0
- package/src/lib/components/editor/extensions/Passage/label.spec.ts +118 -0
- package/src/lib/components/editor/extensions/Passage/label.ts +39 -0
- package/src/lib/components/editor/extensions/Placeholder.ts +9 -0
- package/src/lib/components/editor/extensions/SlashCommand/SlashCommand.ts +65 -0
- package/src/lib/components/editor/extensions/SlashCommand/SuggestionList.tsx +109 -0
- package/src/lib/components/editor/extensions/SlashCommand/Suggestions.ts +185 -0
- package/src/lib/components/editor/extensions/SmallCaps.ts +110 -0
- package/src/lib/components/editor/extensions/StarterKit.ts +36 -0
- package/src/lib/components/editor/extensions/Subscript.ts +43 -0
- package/src/lib/components/editor/extensions/Superscript.ts +43 -0
- package/src/lib/components/editor/extensions/Table.ts +32 -0
- package/src/lib/components/editor/extensions/TextAlign.ts +5 -0
- package/src/lib/components/editor/extensions/TitleMetadata.ts +40 -0
- package/src/lib/components/editor/extensions/TitleNode.ts +133 -0
- package/src/lib/components/editor/extensions/TitlesNode.ts +102 -0
- package/src/lib/components/editor/extensions/Trailer.ts +57 -0
- package/src/lib/components/editor/extensions/TranslationDocument.ts +7 -0
- package/src/lib/components/editor/extensions/TranslationMetadata.ts +58 -0
- package/src/lib/components/editor/extensions/Underline.ts +43 -0
- package/src/lib/components/editor/hooks/index.ts +4 -0
- package/src/lib/components/editor/hooks/useBlockEditor.ts +53 -0
- package/src/lib/components/editor/hooks/useDefaultExtensions.ts +39 -0
- package/src/lib/components/editor/hooks/useDirtyStore.ts +33 -0
- package/src/lib/components/editor/hooks/useTranslationExtensions.ts +148 -0
- package/src/lib/components/editor/index.ts +10 -0
- package/src/lib/components/editor/menus/EmptyBubbleMenu.tsx +42 -0
- package/src/lib/components/editor/menus/MainBubbleMenu.tsx +51 -0
- package/src/lib/components/editor/menus/TranslationBubbleMenu.tsx +57 -0
- package/src/lib/components/editor/menus/index.ts +3 -0
- package/src/lib/components/editor/menus/selectors/EndNoteSelector.tsx +388 -0
- package/src/lib/components/editor/menus/selectors/GlossarySelector.tsx +63 -0
- package/src/lib/components/editor/menus/selectors/LinkSelector.tsx +68 -0
- package/src/lib/components/editor/menus/selectors/MantraSelector.tsx +119 -0
- package/src/lib/components/editor/menus/selectors/NodeSelector.tsx +144 -0
- package/src/lib/components/editor/menus/selectors/ParagraphButtons.tsx +68 -0
- package/src/lib/components/editor/menus/selectors/SelectorInputField.tsx +68 -0
- package/src/lib/components/editor/menus/selectors/TextAlignSelector.tsx +89 -0
- package/src/lib/components/editor/menus/selectors/TextButtons.tsx +89 -0
- package/src/lib/components/editor/menus/selectors/TranslationNodeSelector.tsx +143 -0
- package/src/lib/components/editor/menus/selectors/TranslationTextButtons.tsx +125 -0
- package/src/lib/components/editor/menus/selectors/index.ts +5 -0
- package/src/lib/components/editor/save-filter.spec.ts +94 -0
- package/src/lib/components/editor/save-filter.ts +27 -0
- package/src/lib/components/editor/util.ts +304 -0
- package/src/lib/components/index.ts +3 -0
- package/src/lib/components/reader/ReaderBackMatterPage.tsx +62 -0
- package/src/lib/components/reader/ReaderBackMatterPanel.tsx +53 -0
- package/src/lib/components/reader/ReaderBodyPage.tsx +46 -0
- package/src/lib/components/reader/ReaderBodyPanel.tsx +68 -0
- package/src/lib/components/reader/ReaderLayout.tsx +39 -0
- package/src/lib/components/reader/ReaderLeftPanel.tsx +8 -0
- package/src/lib/components/reader/ReaderLeftPanelPage.tsx +31 -0
- package/src/lib/components/reader/TranslationReader.tsx +28 -0
- package/src/lib/components/reader/index.ts +2 -0
- package/src/lib/components/reader/ssr.ts +3 -0
- package/src/lib/components/shared/AiSummarizerPage.tsx +12 -0
- package/src/lib/components/shared/BackMatterPanel.tsx +143 -0
- package/src/lib/components/shared/BodyPanel.tsx +214 -0
- package/src/lib/components/shared/HoverCardProvider.tsx +407 -0
- package/src/lib/components/shared/Imprint.tsx +24 -0
- package/src/lib/components/shared/LabeledElement.tsx +133 -0
- package/src/lib/components/shared/LeftPanel.tsx +65 -0
- package/src/lib/components/shared/NavigationContext.ts +64 -0
- package/src/lib/components/shared/NavigationProvider.tsx +368 -0
- package/src/lib/components/shared/OpenGraphImage.tsx +75 -0
- package/src/lib/components/shared/PassageSkeleton.tsx +10 -0
- package/src/lib/components/shared/RestrictionWarning.tsx +177 -0
- package/src/lib/components/shared/SourceReader.tsx +83 -0
- package/src/lib/components/shared/SuggestRevisionForm.tsx +99 -0
- package/src/lib/components/shared/TableOfContents.tsx +280 -0
- package/src/lib/components/shared/ThreeColumnRenderer.tsx +54 -0
- package/src/lib/components/shared/TranslationHeader.tsx +86 -0
- package/src/lib/components/shared/TranslationHoverCard.tsx +84 -0
- package/src/lib/components/shared/TranslationSkeleton.tsx +16 -0
- package/src/lib/components/shared/TranslationTable.tsx +155 -0
- package/src/lib/components/shared/bibliography/BibliographyBody.tsx +28 -0
- package/src/lib/components/shared/bibliography/BibliographyList.tsx +63 -0
- package/src/lib/components/shared/bibliography/index.ts +1 -0
- package/src/lib/components/shared/generate-metadata.ts +44 -0
- package/src/lib/components/shared/glossary/GlossaryInstanceBody.tsx +144 -0
- package/src/lib/components/shared/glossary/GlossaryPaginationProvider.tsx +317 -0
- package/src/lib/components/shared/glossary/GlossarySkeleton.tsx +19 -0
- package/src/lib/components/shared/glossary/GlossaryTermList.tsx +58 -0
- package/src/lib/components/shared/glossary/index.ts +3 -0
- package/src/lib/components/shared/hooks/useGlossaryInstanceListener.tsx +42 -0
- package/src/lib/components/shared/hooks/useScrollInTab.tsx +43 -0
- package/src/lib/components/shared/hooks/useScrollPositionRestore.ts +274 -0
- package/src/lib/components/shared/hooks/useTohToggle.tsx +52 -0
- package/src/lib/components/shared/index.ts +11 -0
- package/src/lib/components/shared/ssr.ts +2 -0
- package/src/lib/components/shared/titles/FramedCard.tsx +132 -0
- package/src/lib/components/shared/titles/LongTitle.tsx +20 -0
- package/src/lib/components/shared/titles/LongTitles.tsx +28 -0
- package/src/lib/components/shared/titles/Title.tsx +54 -0
- package/src/lib/components/shared/titles/TitleDetails.tsx +47 -0
- package/src/lib/components/shared/titles/TitleForm.tsx +37 -0
- package/src/lib/components/shared/titles/Titles.tsx +114 -0
- package/src/lib/components/shared/titles/TitlesCard.tsx +113 -0
- package/src/lib/components/shared/titles/index.ts +8 -0
- package/src/lib/components/shared/types.ts +79 -0
- package/src/lib/components/ssr.ts +2 -0
- package/src/lib/exporters/abbreviation.spec.ts +31 -0
- package/src/lib/exporters/abbreviation.ts +22 -0
- package/src/lib/exporters/annotation.ts +193 -0
- package/src/lib/exporters/audio.spec.ts +77 -0
- package/src/lib/exporters/audio.ts +27 -0
- package/src/lib/exporters/blockquote.spec.ts +48 -0
- package/src/lib/exporters/blockquote.ts +24 -0
- package/src/lib/exporters/code.spec.ts +93 -0
- package/src/lib/exporters/code.ts +26 -0
- package/src/lib/exporters/end-note-link.spec.ts +104 -0
- package/src/lib/exporters/end-note-link.ts +35 -0
- package/src/lib/exporters/export.ts +12 -0
- package/src/lib/exporters/glossary-instance.spec.ts +85 -0
- package/src/lib/exporters/glossary-instance.ts +31 -0
- package/src/lib/exporters/has-abbreviation.spec.ts +31 -0
- package/src/lib/exporters/has-abbreviation.ts +21 -0
- package/src/lib/exporters/heading.spec.ts +80 -0
- package/src/lib/exporters/heading.ts +28 -0
- package/src/lib/exporters/image.spec.ts +48 -0
- package/src/lib/exporters/image.ts +25 -0
- package/src/lib/exporters/indent.spec.ts +58 -0
- package/src/lib/exporters/indent.ts +18 -0
- package/src/lib/exporters/index.ts +1 -0
- package/src/lib/exporters/internal-link.spec.ts +90 -0
- package/src/lib/exporters/internal-link.ts +35 -0
- package/src/lib/exporters/italic.spec.ts +84 -0
- package/src/lib/exporters/italic.ts +55 -0
- package/src/lib/exporters/leading-space.spec.ts +28 -0
- package/src/lib/exporters/leading-space.ts +16 -0
- package/src/lib/exporters/line-group.spec.ts +48 -0
- package/src/lib/exporters/line-group.ts +24 -0
- package/src/lib/exporters/line.spec.ts +48 -0
- package/src/lib/exporters/line.ts +24 -0
- package/src/lib/exporters/link.spec.ts +123 -0
- package/src/lib/exporters/link.ts +67 -0
- package/src/lib/exporters/list-item.spec.ts +48 -0
- package/src/lib/exporters/list-item.ts +24 -0
- package/src/lib/exporters/list.spec.ts +82 -0
- package/src/lib/exporters/list.ts +31 -0
- package/src/lib/exporters/mantra.spec.ts +51 -0
- package/src/lib/exporters/mantra.ts +25 -0
- package/src/lib/exporters/mention.ts +41 -0
- package/src/lib/exporters/paragraph.spec.ts +173 -0
- package/src/lib/exporters/paragraph.ts +32 -0
- package/src/lib/exporters/quote.spec.ts +56 -0
- package/src/lib/exporters/quote.ts +25 -0
- package/src/lib/exporters/span.spec.ts +118 -0
- package/src/lib/exporters/span.ts +44 -0
- package/src/lib/exporters/table-body-data.spec.ts +48 -0
- package/src/lib/exporters/table-body-data.ts +24 -0
- package/src/lib/exporters/table-body-header.spec.ts +48 -0
- package/src/lib/exporters/table-body-header.ts +24 -0
- package/src/lib/exporters/table-body-row.spec.ts +48 -0
- package/src/lib/exporters/table-body-row.ts +24 -0
- package/src/lib/exporters/table.spec.ts +48 -0
- package/src/lib/exporters/table.ts +24 -0
- package/src/lib/exporters/trailer.spec.ts +48 -0
- package/src/lib/exporters/trailer.ts +24 -0
- package/src/lib/exporters/util.ts +62 -0
- package/src/lib/passage.ts +182 -0
- package/src/lib/titles.ts +80 -0
- package/src/lib/transformers/abbreviation.spec.ts +87 -0
- package/src/lib/transformers/abbreviation.ts +30 -0
- package/src/lib/transformers/annotate.ts +146 -0
- package/src/lib/transformers/audio.spec.ts +55 -0
- package/src/lib/transformers/audio.ts +29 -0
- package/src/lib/transformers/blockquote.spec.ts +48 -0
- package/src/lib/transformers/blockquote.ts +41 -0
- package/src/lib/transformers/code.spec.ts +52 -0
- package/src/lib/transformers/code.ts +22 -0
- package/src/lib/transformers/deprecated.ts +7 -0
- package/src/lib/transformers/end-note-link.spec.ts +56 -0
- package/src/lib/transformers/end-note-link.ts +76 -0
- package/src/lib/transformers/glossary-instance.spec.ts +55 -0
- package/src/lib/transformers/glossary-instance.ts +40 -0
- package/src/lib/transformers/has-abbreviation.spec.ts +50 -0
- package/src/lib/transformers/has-abbreviation.ts +30 -0
- package/src/lib/transformers/heading.spec.ts +62 -0
- package/src/lib/transformers/heading.ts +30 -0
- package/src/lib/transformers/image.spec.ts +51 -0
- package/src/lib/transformers/image.ts +28 -0
- package/src/lib/transformers/indent.spec.ts +53 -0
- package/src/lib/transformers/indent.ts +17 -0
- package/src/lib/transformers/index.ts +33 -0
- package/src/lib/transformers/inline-title.spec.ts +59 -0
- package/src/lib/transformers/inline-title.ts +34 -0
- package/src/lib/transformers/internal-link.spec.ts +67 -0
- package/src/lib/transformers/internal-link.ts +65 -0
- package/src/lib/transformers/italic.ts +22 -0
- package/src/lib/transformers/leading-space.spec.ts +55 -0
- package/src/lib/transformers/leading-space.ts +17 -0
- package/src/lib/transformers/line-group.spec.ts +48 -0
- package/src/lib/transformers/line-group.ts +37 -0
- package/src/lib/transformers/line.spec.ts +54 -0
- package/src/lib/transformers/line.ts +27 -0
- package/src/lib/transformers/link.spec.ts +61 -0
- package/src/lib/transformers/link.ts +27 -0
- package/src/lib/transformers/list-item.spec.ts +48 -0
- package/src/lib/transformers/list-item.ts +37 -0
- package/src/lib/transformers/list.spec.ts +58 -0
- package/src/lib/transformers/list.ts +42 -0
- package/src/lib/transformers/mantra.spec.ts +58 -0
- package/src/lib/transformers/mantra.ts +28 -0
- package/src/lib/transformers/mention.ts +70 -0
- package/src/lib/transformers/paragraph.spec.ts +46 -0
- package/src/lib/transformers/paragraph.ts +26 -0
- package/src/lib/transformers/quote.spec.ts +42 -0
- package/src/lib/transformers/quote.ts +3 -0
- package/src/lib/transformers/quoted.ts +3 -0
- package/src/lib/transformers/recurse.ts +76 -0
- package/src/lib/transformers/reference.ts +3 -0
- package/src/lib/transformers/span.spec.ts +68 -0
- package/src/lib/transformers/span.ts +78 -0
- package/src/lib/transformers/split-at.ts +58 -0
- package/src/lib/transformers/split-block.ts +110 -0
- package/src/lib/transformers/split-content.ts +67 -0
- package/src/lib/transformers/split-insert.ts +76 -0
- package/src/lib/transformers/split-marks.ts +42 -0
- package/src/lib/transformers/split-node.ts +138 -0
- package/src/lib/transformers/table-body-data.spec.ts +44 -0
- package/src/lib/transformers/table-body-data.ts +29 -0
- package/src/lib/transformers/table-body-header.spec.ts +44 -0
- package/src/lib/transformers/table-body-header.ts +29 -0
- package/src/lib/transformers/table-body-row.spec.ts +44 -0
- package/src/lib/transformers/table-body-row.ts +29 -0
- package/src/lib/transformers/table.spec.ts +47 -0
- package/src/lib/transformers/table.ts +29 -0
- package/src/lib/transformers/trailer.spec.ts +43 -0
- package/src/lib/transformers/trailer.ts +26 -0
- package/src/lib/transformers/transformer.ts +25 -0
- package/src/lib/transformers/unknown.ts +8 -0
- package/src/lib/transformers/util.ts +20 -0
- package/src/lib/types.ts +10 -0
- package/src/ssr.ts +1 -0
- package/tsconfig.json +20 -0
- package/tsconfig.lib.json +29 -0
- 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);
|