@portone/docx-editor 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/DocxEditor.js +84 -26
  3. package/dist/docx/exportDocx.d.ts +2 -1
  4. package/dist/docx/exportDocx.js +3 -9
  5. package/dist/docx/headersFooters.js +35 -23
  6. package/dist/docx/identities.d.ts +28 -0
  7. package/dist/docx/identities.js +42 -10
  8. package/dist/docx/importDocx.js +25 -26
  9. package/dist/docx/importParagraph.d.ts +1 -4
  10. package/dist/docx/importParagraph.js +4 -18
  11. package/dist/docx/invariants.d.ts +1 -1
  12. package/dist/docx/invariants.js +45 -4
  13. package/dist/docx/notes/newNote.d.ts +39 -0
  14. package/dist/docx/notes/newNote.js +122 -0
  15. package/dist/docx/notes/numbering.d.ts +27 -0
  16. package/dist/docx/notes/numbering.js +120 -0
  17. package/dist/docx/notes/reading.d.ts +58 -0
  18. package/dist/docx/notes/reading.js +98 -0
  19. package/dist/docx/notes/writing.d.ts +19 -0
  20. package/dist/docx/notes/writing.js +96 -0
  21. package/dist/docx/partPlan.d.ts +3 -0
  22. package/dist/docx/partPlanners.d.ts +13 -0
  23. package/dist/docx/partPlanners.js +48 -0
  24. package/dist/docx/sections.d.ts +5 -0
  25. package/dist/docx/sections.js +7 -2
  26. package/dist/docx/serializeParagraph.js +7 -2
  27. package/dist/docx/session.d.ts +5 -0
  28. package/dist/docx/session.js +6 -0
  29. package/dist/docx/storyParts.d.ts +112 -0
  30. package/dist/docx/storyParts.js +289 -0
  31. package/dist/editor/clipboard/htmlReader.d.ts +24 -4
  32. package/dist/editor/clipboard/htmlReader.js +39 -4
  33. package/dist/editor/clipboard/internalChannel.d.ts +30 -3
  34. package/dist/editor/clipboard/internalChannel.js +43 -3
  35. package/dist/editor/clipboard/normalizers.d.ts +35 -8
  36. package/dist/editor/clipboard/normalizers.js +60 -11
  37. package/dist/editor/clipboard/plugin.js +42 -22
  38. package/dist/editor/commands/index.d.ts +1 -0
  39. package/dist/editor/commands/index.js +18 -0
  40. package/dist/editor/commands/noteCommands.d.ts +47 -0
  41. package/dist/editor/commands/noteCommands.js +95 -0
  42. package/dist/editor/commands/noteQueries.d.ts +22 -3
  43. package/dist/editor/commands/noteQueries.js +21 -13
  44. package/dist/editor/createEditor.js +21 -3
  45. package/dist/editor/documentStyles.d.ts +3 -0
  46. package/dist/editor/documentStyles.js +2 -1
  47. package/dist/editor/editorDocument.d.ts +24 -0
  48. package/dist/editor/editorDocument.js +34 -4
  49. package/dist/editor/notes/noteSurface.d.ts +53 -0
  50. package/dist/editor/notes/noteSurface.js +246 -0
  51. package/dist/editor/plugins/compositionSelection.d.ts +20 -0
  52. package/dist/editor/plugins/compositionSelection.js +19 -0
  53. package/dist/editor/plugins/keymap.d.ts +14 -0
  54. package/dist/editor/plugins/keymap.js +13 -1
  55. package/dist/editor/plugins/noteLifecycle.d.ts +18 -0
  56. package/dist/editor/plugins/noteLifecycle.js +117 -0
  57. package/dist/editor/plugins/noteNavigation.d.ts +45 -0
  58. package/dist/editor/plugins/noteNavigation.js +86 -0
  59. package/dist/editor/plugins/noteNumbering.d.ts +13 -0
  60. package/dist/editor/plugins/noteNumbering.js +58 -0
  61. package/dist/editor/plugins/rowResize.js +2 -7
  62. package/dist/editor/plugins/tabLayout.js +2 -7
  63. package/dist/editor/stories/storyMarkup.d.ts +24 -0
  64. package/dist/editor/stories/storyMarkup.js +35 -0
  65. package/dist/editor/stories/storyState.d.ts +34 -0
  66. package/dist/editor/stories/storyState.js +74 -0
  67. package/dist/editor/stories/storyView.d.ts +115 -0
  68. package/dist/editor/stories/storyView.js +144 -0
  69. package/dist/numbering/spellers.d.ts +1 -1
  70. package/dist/numbering/spellers.js +6 -1
  71. package/dist/page/blockKinds.d.ts +23 -4
  72. package/dist/page/demands/footnoteDemands.d.ts +18 -0
  73. package/dist/page/demands/footnoteDemands.js +51 -0
  74. package/dist/page/demands/index.d.ts +43 -0
  75. package/dist/page/demands/index.js +8 -0
  76. package/dist/page/kinds/paragraphKind.js +7 -1
  77. package/dist/page/kinds/tableKind.js +9 -1
  78. package/dist/page/measureBlocks.d.ts +2 -0
  79. package/dist/page/measureBlocks.js +31 -10
  80. package/dist/page/pageDecorations.d.ts +9 -2
  81. package/dist/page/pageDecorations.js +18 -5
  82. package/dist/page/pageLayout.d.ts +77 -1
  83. package/dist/page/pageLayout.js +186 -9
  84. package/dist/page/usePageLayout.d.ts +57 -2
  85. package/dist/page/usePageLayout.js +50 -5
  86. package/dist/schema/attrRoles.js +3 -2
  87. package/dist/schema/docxSchema.js +5 -2
  88. package/dist/schema/editGuard.d.ts +1 -1
  89. package/dist/schema/guards.js +1 -2
  90. package/dist/schema/preservedGuards.d.ts +4 -6
  91. package/dist/schema/preservedGuards.js +0 -9
  92. package/dist/schema/stories.d.ts +34 -2
  93. package/dist/schema/stories.js +22 -5
  94. package/dist/styles/classNames.d.ts +21 -6
  95. package/dist/styles/classNames.js +21 -6
  96. package/dist/styles/inlineStyle.d.ts +7 -0
  97. package/dist/styles/inlineStyle.js +11 -5
  98. package/dist/styles/visualScale.d.ts +19 -0
  99. package/dist/styles/visualScale.js +26 -0
  100. package/dist/styles.css +89 -32
  101. package/dist/ui/InsertImageButton.d.ts +3 -1
  102. package/dist/ui/InsertImageButton.js +3 -2
  103. package/dist/ui/TableMenu.d.ts +5 -2
  104. package/dist/ui/TableMenu.js +18 -1
  105. package/dist/ui/TextMenu.d.ts +5 -2
  106. package/dist/ui/TextMenu.js +21 -6
  107. package/dist/ui/Toolbar.d.ts +17 -2
  108. package/dist/ui/Toolbar.js +23 -10
  109. package/dist/ui/comments/useCommentRailLayout.js +2 -4
  110. package/dist/ui/noteItems.d.ts +21 -0
  111. package/dist/ui/noteItems.js +30 -0
  112. package/dist/ui/notes/NoteAreas.d.ts +73 -0
  113. package/dist/ui/notes/NoteAreas.js +131 -0
  114. package/dist/ui/notes/NoteList.d.ts +25 -0
  115. package/dist/ui/notes/NoteList.js +58 -0
  116. package/dist/ui/notes/StoryRow.d.ts +75 -0
  117. package/dist/ui/notes/StoryRow.js +122 -0
  118. package/dist/ui/notes/noteBands.d.ts +100 -0
  119. package/dist/ui/notes/noteBands.js +142 -0
  120. package/dist/ui/notes/noteHeights.d.ts +10 -0
  121. package/dist/ui/notes/noteHeights.js +24 -0
  122. package/dist/ui/notes/noteSeparator.d.ts +12 -0
  123. package/dist/ui/notes/noteSeparator.js +9 -0
  124. package/dist/ui/notes/useStorySurface.d.ts +68 -0
  125. package/dist/ui/notes/useStorySurface.js +95 -0
  126. package/dist/ui/runCommand.d.ts +8 -1
  127. package/dist/ui/runCommand.js +2 -2
  128. package/dist/ui/shortcutLabels.d.ts +16 -0
  129. package/dist/ui/shortcutLabels.js +9 -0
  130. package/dist/ui/usePageRoom.d.ts +13 -0
  131. package/dist/ui/usePageRoom.js +31 -0
  132. package/package.json +1 -1
  133. package/dist/docx/notes.d.ts +0 -27
  134. package/dist/docx/notes.js +0 -55
  135. package/dist/ui/NotesPanel.d.ts +0 -6
  136. package/dist/ui/NotesPanel.js +0 -33
@@ -10,9 +10,11 @@
10
10
  import type { Node as PMNode } from "prosemirror-model";
11
11
  import { type EditorState, Plugin } from "prosemirror-state";
12
12
  import { type FormattingContext, type ParagraphStyleOption } from "../docx/formatting";
13
+ import { type NoteNumbering } from "../docx/notes/reading";
13
14
  import { type PageGeometry } from "../docx/pageGeometry";
14
15
  import type { SessionStore } from "../docx/session";
15
16
  import type { DocumentDefaults } from "../model/format";
17
+ import { type StoryKey } from "../schema/stories";
16
18
  /** The document-level values one editing state is built on */
17
19
  export interface EditorDocument {
18
20
  /** The document these values were read from. Null for a state built without one */
@@ -30,12 +32,26 @@ export interface EditorDocument {
30
32
  readonly canStartNewList: boolean;
31
33
  /** The paper the document is written on, which decides the width a new table is fitted to */
32
34
  readonly geometry: PageGeometry;
35
+ /**
36
+ * Whether these values stand for a side story rather than for the body, whose paper is then the
37
+ * geometry above rather than anything the story's own blocks say (`editor/documentStyles`).
38
+ */
39
+ readonly sideStory: boolean;
33
40
  /** The distance between automatic tab stops when no custom stop applies */
34
41
  readonly defaultTabStopPt: number;
35
42
  /** Every id already present in the opened Comments part, including unreferenced entries */
36
43
  readonly reservedCommentIds: ReadonlySet<string>;
37
44
  /** Every paragraph id present in the opened comment parts, including orphan extension entries */
38
45
  readonly reservedCommentParaIds: ReadonlySet<string>;
46
+ /** How the document counts its footnotes and endnotes, which the note labels are spelled in */
47
+ readonly noteNumbering: NoteNumbering;
48
+ /** The note entries that lay out the page rather than number a note: separators and the continuation notice */
49
+ readonly specialNotes: ReadonlySet<StoryKey>;
50
+ /**
51
+ * Every entry of an editable kind the opened notes parts hold, separators and entries no
52
+ * reference names included, which a new note may not take the id of
53
+ */
54
+ readonly reservedNoteKeys: ReadonlySet<StoryKey>;
39
55
  }
40
56
  /** What a state built without an opened document answers: the same as nothing being written down */
41
57
  export declare const NO_DOCUMENT: EditorDocument;
@@ -51,6 +67,14 @@ export declare function documentOf(state: EditorState): EditorDocument;
51
67
  * both leave the same object behind, allowing the sheet's style to be cached against that identity.
52
68
  */
53
69
  export declare function editorDocument(document: EditorDocument): Plugin<EditorDocument>;
70
+ /**
71
+ * The snapshot one side story is edited against: the document's own values, with the two a story
72
+ * answers differently.
73
+ *
74
+ * A story has nowhere of its own to write a list definition, and the paper its text wraps at is
75
+ * the paper of the place it is called from rather than anything the story says.
76
+ */
77
+ export declare function storyDocument(document: EditorDocument, geometry: PageGeometry): EditorDocument;
54
78
  /** The comment ids the opened document already spent, which a new comment may not take */
55
79
  export declare function reservedCommentIds(state: EditorState): ReadonlySet<string>;
56
80
  /** The paragraph ids the opened comment parts already spent */
@@ -6,9 +6,17 @@ import {
6
6
  NO_FORMATTING
7
7
  } from "../docx/formatting.js";
8
8
  import { canDefineNewList } from "../docx/newLists.js";
9
+ import {
10
+ DEFAULT_NOTE_NUMBERING
11
+ } from "../docx/notes/reading.js";
9
12
  import { A4_PORTRAIT } from "../docx/pageGeometry.js";
10
13
  import { NEW_LISTS_ATTR, newListsOf } from "../numbering/listRegistry.js";
14
+ import {
15
+ EDITABLE_NOTE_KINDS,
16
+ STORIES_ATTR
17
+ } from "../schema/stories.js";
11
18
  var NO_IDS = /* @__PURE__ */ new Set();
19
+ var NO_NOTE_KEYS = /* @__PURE__ */ new Set();
12
20
  var NO_PARAGRAPH_STYLES = [];
13
21
  var NO_DOCUMENT = {
14
22
  session: null,
@@ -17,9 +25,13 @@ var NO_DOCUMENT = {
17
25
  paragraphStyles: NO_PARAGRAPH_STYLES,
18
26
  canStartNewList: true,
19
27
  geometry: A4_PORTRAIT,
28
+ sideStory: false,
20
29
  defaultTabStopPt: DEFAULT_TAB_STOP_PT,
21
30
  reservedCommentIds: NO_IDS,
22
- reservedCommentParaIds: NO_IDS
31
+ reservedCommentParaIds: NO_IDS,
32
+ noteNumbering: DEFAULT_NOTE_NUMBERING,
33
+ specialNotes: NO_NOTE_KEYS,
34
+ reservedNoteKeys: NO_NOTE_KEYS
23
35
  };
24
36
  function reservedParaIds(session) {
25
37
  const paraIds = /* @__PURE__ */ new Set();
@@ -46,9 +58,17 @@ function editorDocumentOf(session, doc) {
46
58
  paragraphStyles: session.paragraphStyles,
47
59
  canStartNewList: canDefineNewList(session),
48
60
  geometry: session.geometry,
61
+ sideStory: false,
49
62
  defaultTabStopPt: session.defaultTabStopPt,
50
63
  reservedCommentIds: new Set(session.comments.byId.keys()),
51
- reservedCommentParaIds: reservedParaIds(session)
64
+ reservedCommentParaIds: reservedParaIds(session),
65
+ noteNumbering: session.noteNumbering,
66
+ specialNotes: session.specialNotes,
67
+ reservedNoteKeys: new Set(
68
+ Array.from(session.stories.values()).flatMap(
69
+ (story) => EDITABLE_NOTE_KINDS.some((kind) => kind === story.kind) ? [story.key] : []
70
+ )
71
+ )
52
72
  };
53
73
  }
54
74
  var editorDocumentKey = new PluginKey("docxEditorDocument");
@@ -61,15 +81,24 @@ function derived(current, doc) {
61
81
  formatting: formattingWithNewLists(current.formatting, doc)
62
82
  } : editorDocumentOf(current.session, doc);
63
83
  }
84
+ function onlyStoriesMoved(next, before) {
85
+ const names = Object.keys(next.attrs);
86
+ return next.attrs[STORIES_ATTR] !== before.attrs[STORIES_ATTR] && names.length === Object.keys(before.attrs).length && names.every(
87
+ (name) => name === STORIES_ATTR || next.attrs[name] === before.attrs[name]
88
+ );
89
+ }
64
90
  function editorDocument(document) {
65
91
  return new Plugin({
66
92
  key: editorDocumentKey,
67
93
  state: {
68
94
  init: () => document,
69
- apply: (tr, current, old) => tr.doc.attrs === old.doc.attrs ? current : derived(current, tr.doc)
95
+ apply: (tr, current, old) => tr.doc.attrs === old.doc.attrs || onlyStoriesMoved(tr.doc, old.doc) ? current : derived(current, tr.doc)
70
96
  }
71
97
  });
72
98
  }
99
+ function storyDocument(document, geometry) {
100
+ return { ...document, sideStory: true, canStartNewList: false, geometry };
101
+ }
73
102
  function reservedCommentIds(state) {
74
103
  return documentOf(state).reservedCommentIds;
75
104
  }
@@ -82,5 +111,6 @@ export {
82
111
  editorDocument,
83
112
  editorDocumentOf,
84
113
  reservedCommentIds,
85
- reservedCommentParaIds
114
+ reservedCommentParaIds,
115
+ storyDocument
86
116
  };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * What a footnote or an endnote draws and does differently from any other story.
3
+ *
4
+ * A note's story opens with a mark of its own (`w:footnoteRef`, `w:endnoteRef`), which the file
5
+ * keeps as a preserved chip. Word draws that mark as the number the reference carries, so it is
6
+ * drawn here as that label in superscript, the way the reference in the body is.
7
+ *
8
+ * The rest is what the story view asks a kind of story for (`editor/stories`): the host every
9
+ * edit leaves through, and the extensions that give a note its chip, its key rule, the number it
10
+ * keeps through an edit, and what a paste into one may carry.
11
+ */
12
+ import type { EditorView } from "prosemirror-view";
13
+ import { type StoryKey } from "../../schema/stories";
14
+ import { type SliceNormalizer } from "../clipboard/normalizers";
15
+ import type { StoryNodeSpecs } from "../stories/storyMarkup";
16
+ import { type StoryExtensions, type StoryHost } from "../stories/storyView";
17
+ /** The node specs a note's story is drawn with, its own reference mark drawn as `labelOf` answers */
18
+ export declare function noteNodeSpecs(labelOf: () => string): StoryNodeSpecs;
19
+ /**
20
+ * Takes out of a paste what the notes part cannot be written with: an image, and a link.
21
+ *
22
+ * Either needs a relationship of the part it stands in, and the notes part writer writes none, so
23
+ * both would go out naming a relationship the file does not hold. A link or an image the file
24
+ * itself wrote inside a note stays; this is about what a paste would add.
25
+ */
26
+ export declare const dropUnwritableContent: SliceNormalizer;
27
+ /**
28
+ * Takes a note reference out of a paste: a note may not hold a note.
29
+ *
30
+ * The lifecycle, the numbering and the writer all walk the body alone, so a reference inside a
31
+ * story would call a note nothing numbers, counts or settles, and one copied from beside the note
32
+ * it was pasted into would have that note calling itself. The text it stood in stays, the way the
33
+ * other anchors leave theirs (`editor/clipboard/normalizers`).
34
+ */
35
+ export declare const dropNoteReferences: SliceNormalizer;
36
+ /**
37
+ * Puts the caret back just after the reference that calls this note and takes the body's focus,
38
+ * which is what leaves the note: Escape and a press on the note's own number both run it.
39
+ */
40
+ export declare function returnToReference(main: EditorView, key: StoryKey): void;
41
+ /**
42
+ * The host a note's editing view writes through, whichever kind of note stands under the key.
43
+ *
44
+ * Every edit leaves as that kind's own body command, so a lock around the reference and the
45
+ * editing protection judge a note edit where they judge a body edit, and the main document keeps
46
+ * the one history both share.
47
+ */
48
+ export declare function noteHost(main: EditorView, activate: StoryHost["activate"]): StoryHost;
49
+ /**
50
+ * What a note adds to the story view: its chip, the number it keeps first, its key rule, and what
51
+ * a paste may bring in
52
+ */
53
+ export declare function noteExtensions(main: EditorView, key: StoryKey, labelOf: () => string): StoryExtensions;
@@ -0,0 +1,246 @@
1
+ // src/editor/notes/noteSurface.ts
2
+ import { keymap } from "prosemirror-keymap";
3
+ import {
4
+ DOMSerializer,
5
+ Mark
6
+ } from "prosemirror-model";
7
+ import { Plugin, TextSelection } from "prosemirror-state";
8
+ import { NOTE_NUMBER_ELEMENTS } from "../../docx/notes/newNote.js";
9
+ import { docxSchema } from "../../schema/index.js";
10
+ import { editShut, transactionAllowed } from "../../schema/guards.js";
11
+ import { editsShut } from "../../schema/protectionState.js";
12
+ import { noteKeyOf } from "../../schema/stories.js";
13
+ import { editorClassNames } from "../../styles/classNames.js";
14
+ import {
15
+ detachAnchors,
16
+ dropSourceIdentity,
17
+ mapSliceNodes,
18
+ rederiveDisplay,
19
+ rekeyNumbering
20
+ } from "../clipboard/normalizers.js";
21
+ import { redo, undo } from "../commands/historyCommands.js";
22
+ import { noteBodyCommand } from "../commands/noteCommands.js";
23
+ import { onMac } from "../plugins/keymap.js";
24
+ import { noteReferenceAt } from "../plugins/noteNavigation.js";
25
+ import {
26
+ NO_CAPABILITY
27
+ } from "../stories/storyView.js";
28
+ function isOwnMark(node) {
29
+ const element = node.attrs.element;
30
+ return node.type === docxSchema.nodes.rawRunContent && typeof element === "string" && NOTE_NUMBER_ELEMENTS.includes(element);
31
+ }
32
+ function drawnBySchema(node) {
33
+ return docxSchema.nodes.rawRunContent.spec.toDOM?.(node) ?? ["span"];
34
+ }
35
+ function markSpec(node, labelOf) {
36
+ return isOwnMark(node) ? ["sup", { class: editorClassNames.noteMark }, labelOf()] : drawnBySchema(node);
37
+ }
38
+ function noteNodeSpecs(labelOf) {
39
+ return { rawRunContent: (node) => markSpec(node, labelOf) };
40
+ }
41
+ function noteMarkView(labelOf, back) {
42
+ return (node) => {
43
+ const { dom } = DOMSerializer.renderSpec(document, markSpec(node, labelOf));
44
+ if (isOwnMark(node) && dom instanceof HTMLElement) {
45
+ dom.addEventListener("mousedown", (event) => {
46
+ event.preventDefault();
47
+ back();
48
+ });
49
+ }
50
+ return { dom };
51
+ };
52
+ }
53
+ var dropUnwritableContent = (slice) => mapSliceNodes(slice, (node) => {
54
+ if (node.type === docxSchema.nodes.image) return null;
55
+ const marks = node.marks.filter(
56
+ (mark) => mark.type !== docxSchema.marks.link
57
+ );
58
+ return Mark.sameSet(marks, node.marks) ? node : node.mark(marks);
59
+ });
60
+ var dropNoteReferences = (slice, { move }) => move ? slice : mapSliceNodes(
61
+ slice,
62
+ (node) => node.type === docxSchema.nodes.noteReference ? null : node
63
+ );
64
+ var NOTE_TAKES = NO_CAPABILITY;
65
+ var NOTE_NORMALIZERS = [
66
+ dropSourceIdentity,
67
+ detachAnchors,
68
+ dropNoteReferences,
69
+ dropUnwritableContent,
70
+ rekeyNumbering,
71
+ rederiveDisplay
72
+ ];
73
+ function runOn(main, command) {
74
+ return command(main.state, (tr) => main.dispatch(tr), main);
75
+ }
76
+ function referenceOf(main, key) {
77
+ const note = noteKeyOf(key);
78
+ return note === null ? null : noteReferenceAt(main.state.doc, note.kind, note.id);
79
+ }
80
+ function returnToReference(main, key) {
81
+ const found = referenceOf(main, key);
82
+ if (found !== null) {
83
+ const after = found.pos + found.node.nodeSize;
84
+ main.dispatch(
85
+ main.state.tr.setSelection(TextSelection.near(main.state.doc.resolve(after))).scrollIntoView()
86
+ );
87
+ }
88
+ main.focus();
89
+ }
90
+ function leadingChipSize(story) {
91
+ const first = story.firstChild?.firstChild ?? null;
92
+ return first !== null && isOwnMark(first) ? first.nodeSize : 0;
93
+ }
94
+ function ownMarkIn(story) {
95
+ let found = null;
96
+ story.descendants((node, pos) => {
97
+ if (found === null && isOwnMark(node)) found = { pos, node };
98
+ return found === null;
99
+ });
100
+ return found;
101
+ }
102
+ function caretFloor(story) {
103
+ const held = ownMarkIn(story);
104
+ return held === null || held.pos !== markHome(story) ? null : held.pos + held.node.nodeSize;
105
+ }
106
+ function markHome(story) {
107
+ const type = docxSchema.nodes.rawRunContent;
108
+ let at = null;
109
+ story.descendants((node, pos) => {
110
+ if (at !== null) return false;
111
+ if (!node.isTextblock) return true;
112
+ if (node.type.contentMatch.matchType(type) !== null) at = pos + 1;
113
+ return false;
114
+ });
115
+ return at;
116
+ }
117
+ function pressesTowardsTheNumber() {
118
+ return [
119
+ "ArrowLeft",
120
+ "ArrowUp",
121
+ "Home",
122
+ "PageUp",
123
+ "Mod-ArrowLeft",
124
+ "Mod-ArrowUp",
125
+ "Mod-Home",
126
+ ...onMac() ? ["Ctrl-a"] : []
127
+ ];
128
+ }
129
+ function holdAtTheNumber(extend) {
130
+ return (state, dispatch) => {
131
+ const floor = caretFloor(state.doc);
132
+ const home = markHome(state.doc);
133
+ if (floor === null || home === null) return false;
134
+ const { selection } = state;
135
+ if (extend ? selection.head > home : selection.from > floor) return false;
136
+ if (dispatch && !extend) {
137
+ dispatch(state.tr.setSelection(TextSelection.create(state.doc, floor)));
138
+ }
139
+ return true;
140
+ };
141
+ }
142
+ function keysOffTheNumber() {
143
+ return Object.fromEntries(
144
+ pressesTowardsTheNumber().flatMap((press) => [
145
+ [press, holdAtTheNumber(false)],
146
+ [`Shift-${press}`, holdAtTheNumber(true)]
147
+ ])
148
+ );
149
+ }
150
+ function ownMarkFirst() {
151
+ return new Plugin({
152
+ appendTransaction(transactions, oldState, newState) {
153
+ const tr = newState.tr;
154
+ const home = markHome(newState.doc);
155
+ if (transactions.some((changed) => changed.docChanged) && home !== null) {
156
+ const held = ownMarkIn(newState.doc);
157
+ const lost = ownMarkIn(oldState.doc);
158
+ if (held === null) {
159
+ if (lost !== null) tr.insert(home, lost.node);
160
+ } else if (held.pos !== home) {
161
+ tr.delete(held.pos, held.pos + held.node.nodeSize).insert(
162
+ home,
163
+ held.node
164
+ );
165
+ }
166
+ }
167
+ const floor = caretFloor(tr.doc);
168
+ if (floor !== null && tr.selection.empty && tr.selection.from < floor) {
169
+ tr.setSelection(
170
+ TextSelection.create(tr.doc, Math.min(floor, tr.doc.content.size))
171
+ );
172
+ }
173
+ return tr.docChanged || tr.selectionSet ? tr : null;
174
+ }
175
+ });
176
+ }
177
+ function holdsNothing(story) {
178
+ const only = story.childCount === 1 ? story.firstChild : null;
179
+ return only !== null && only.type === docxSchema.nodes.paragraph && only.content.size === leadingChipSize(story);
180
+ }
181
+ function deleteEmptyNote(main, key) {
182
+ return (state, dispatch) => {
183
+ if (!state.selection.empty) return false;
184
+ if (!holdsNothing(state.doc)) return false;
185
+ const $at = state.selection.$from;
186
+ if ($at.index(0) !== 0 || $at.parentOffset > leadingChipSize(state.doc)) {
187
+ return false;
188
+ }
189
+ const found = referenceOf(main, key);
190
+ if (found === null) return false;
191
+ const tr = main.state.tr.delete(found.pos, found.pos + found.node.nodeSize);
192
+ if (!transactionAllowed(tr, main.state)) return false;
193
+ if (dispatch) {
194
+ main.dispatch(
195
+ tr.setSelection(TextSelection.near(tr.doc.resolve(found.pos))).scrollIntoView()
196
+ );
197
+ main.focus();
198
+ }
199
+ return true;
200
+ };
201
+ }
202
+ function noteHost(main, activate) {
203
+ return {
204
+ state: () => main.state,
205
+ write(key, story) {
206
+ const note = noteKeyOf(key);
207
+ return note !== null && runOn(main, noteBodyCommand(note.kind, note.id, story));
208
+ },
209
+ undo: () => runOn(main, undo),
210
+ redo: () => runOn(main, redo),
211
+ leave: (key) => returnToReference(main, key),
212
+ shut(key) {
213
+ if (editsShut(main.state)) return true;
214
+ const found = referenceOf(main, key);
215
+ if (found === null) return true;
216
+ return editShut(main.state, {
217
+ kind: "replace",
218
+ from: found.pos,
219
+ to: found.pos + found.node.nodeSize
220
+ });
221
+ },
222
+ activate
223
+ };
224
+ }
225
+ function noteExtensions(main, key, labelOf) {
226
+ return {
227
+ plugins: [
228
+ ownMarkFirst(),
229
+ keymap(keysOffTheNumber()),
230
+ keymap({ Backspace: deleteEmptyNote(main, key) })
231
+ ],
232
+ nodeViews: {
233
+ rawRunContent: noteMarkView(labelOf, () => returnToReference(main, key))
234
+ },
235
+ normalizers: NOTE_NORMALIZERS,
236
+ takes: NOTE_TAKES
237
+ };
238
+ }
239
+ export {
240
+ dropNoteReferences,
241
+ dropUnwritableContent,
242
+ noteExtensions,
243
+ noteHost,
244
+ noteNodeSpecs,
245
+ returnToReference
246
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Takes the selected text away before a composition opens over it.
3
+ *
4
+ * A composition normally replaces what is selected because the browser does the replacing and
5
+ * ProseMirror reads the result back. Chrome will not touch a selection that begins at an element
6
+ * it may not edit - the number a note is drawn by, a note reference - and does not say so: the
7
+ * composition lands beside the words it was meant to replace, or nothing happens at all. A note
8
+ * meets it every time, since its first paragraph opens with its number and selecting the whole of
9
+ * a note to write it again is an ordinary thing to do.
10
+ *
11
+ * So the selection is deleted here, in the frame the composition opens in and before ProseMirror
12
+ * marks the view as composing, which leaves the composition starting from a caret with nothing
13
+ * for the browser to refuse. ProseMirror does the same for a selection spanning whole blocks
14
+ * (`endComposition`).
15
+ *
16
+ * The deletion is an edit like any other, so the guards judge it: across a bookmark marker it is
17
+ * refused, and the composition is left to the browser where it stood.
18
+ */
19
+ import { Plugin } from "prosemirror-state";
20
+ export declare function compositionSelection(): Plugin;
@@ -0,0 +1,19 @@
1
+ // src/editor/plugins/compositionSelection.ts
2
+ import { deleteSelection } from "prosemirror-commands";
3
+ import { Plugin } from "prosemirror-state";
4
+ function compositionSelection() {
5
+ return new Plugin({
6
+ props: {
7
+ handleDOMEvents: {
8
+ compositionstart(view) {
9
+ if (view.state.selection.empty) return false;
10
+ deleteSelection(view.state, (tr) => view.dispatch(tr));
11
+ return false;
12
+ }
13
+ }
14
+ }
15
+ });
16
+ }
17
+ export {
18
+ compositionSelection
19
+ };
@@ -2,6 +2,7 @@
2
2
  * Keys not handled here are taken by ProseMirror's base keymap.
3
3
  */
4
4
  import { type Command, Plugin } from "prosemirror-state";
5
+ import type { NoteKind } from "../../schema/stories";
5
6
  /**
6
7
  * Keeps undo and redo on their keys where the keymap above cannot reach them.
7
8
  *
@@ -12,4 +13,17 @@ import { type Command, Plugin } from "prosemirror-state";
12
13
  * second time.
13
14
  */
14
15
  export declare function historyKeys(): Plugin;
16
+ /**
17
+ * Whether the editor is running on a Mac, which decides the Mod key and what the system takes
18
+ * before a page sees it.
19
+ */
20
+ export declare function onMac(): boolean;
21
+ /**
22
+ * The key each kind of note is put in on.
23
+ *
24
+ * They are Word's own, except that Word for Windows' endnote key is the system's on a Mac:
25
+ * Cmd+Option+D hides the Dock, so the editor is never asked about it, and Word for Mac puts an
26
+ * endnote on Cmd+Option+E. The footnote key is the same on both, which is Google Docs' as well.
27
+ */
28
+ export declare const NOTE_KEYS: Readonly<Record<NoteKind, string>>;
15
29
  export declare const docxKeymap: Record<string, Command>;
@@ -22,6 +22,7 @@ import {
22
22
  increaseListLevel,
23
23
  leaveEmptyListItem
24
24
  } from "../commands/listCommands.js";
25
+ import { insertEndnote, insertFootnote } from "../commands/noteCommands.js";
25
26
  import { insertTab, moveAcrossTab } from "../commands/tabCommands.js";
26
27
  import { openLinkPanel } from "./linkPanel.js";
27
28
  var insertTabFromKeyboard = (state, dispatch, view) => {
@@ -91,6 +92,13 @@ function historyKeys() {
91
92
  }
92
93
  });
93
94
  }
95
+ function onMac() {
96
+ return typeof navigator !== "undefined" && navigator.platform.includes("Mac");
97
+ }
98
+ var NOTE_KEYS = {
99
+ footnote: "Mod-Alt-f",
100
+ endnote: onMac() ? "Mod-Alt-e" : "Mod-Alt-d"
101
+ };
94
102
  var docxKeymap = {
95
103
  Enter: chainCommands(leaveEmptyListItem, splitParagraph),
96
104
  "Shift-Enter": insertLineBreak,
@@ -107,6 +115,8 @@ var docxKeymap = {
107
115
  // The link key Word and Google Docs share. It opens the panel over the selection, and with
108
116
  // nothing there to link it reports that it did nothing, so the browser keeps its own Cmd+K
109
117
  "Mod-k": openLinkPanel,
118
+ [NOTE_KEYS.footnote]: insertFootnote,
119
+ [NOTE_KEYS.endnote]: insertEndnote,
110
120
  // Inside a table, moving between cells comes first (Word does the same).
111
121
  // In a list paragraph outside a table it shifts the level; an ordinary paragraph gets a document tab.
112
122
  Tab: chainCommands(
@@ -124,6 +134,8 @@ var docxKeymap = {
124
134
  "Shift-ArrowRight": moveAcrossTab("right", true)
125
135
  };
126
136
  export {
137
+ NOTE_KEYS,
127
138
  docxKeymap,
128
- historyKeys
139
+ historyKeys,
140
+ onMac
129
141
  };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Keeps each note of an editable kind in step with the references that call it.
3
+ *
4
+ * A note is written once and called from the text by its id, so what an edit does to the text
5
+ * decides what becomes of the note. An edit that takes away the last reference to a note takes the
6
+ * note away with it. An edit that copies a reference gives the copy a new id and a copy of the
7
+ * note, since two references naming one id are one note drawn with one number where the reader
8
+ * made two. A reference moved whole keeps its id and its note. Either change goes in a transaction
9
+ * appended to the edit, so it lands in the edit's history event and one undo takes both back.
10
+ *
11
+ * Only what the edit did is read. An entry the file arrived with and no reference to is never
12
+ * reached, a reference naming no note is left naming none, and the separators a notes part lays
13
+ * the page out with are left as they are. The notes part writer relies on the first of these: a
14
+ * note disappears only together with its last reference (`docx/storyParts`).
15
+ */
16
+ import { Plugin } from "prosemirror-state";
17
+ import { type NoteKind } from "../../schema/stories";
18
+ export declare function noteLifecycle(kinds?: readonly NoteKind[]): Plugin;
@@ -0,0 +1,117 @@
1
+ // src/editor/plugins/noteLifecycle.ts
2
+ import { Plugin } from "prosemirror-state";
3
+ import { DocAttrStep, Mapping } from "prosemirror-transform";
4
+ import {
5
+ copiedNoteStory,
6
+ nextNoteId,
7
+ takenNoteIds
8
+ } from "../../docx/notes/newNote.js";
9
+ import { setStory, withoutStories } from "../../docx/story.js";
10
+ import { displayOnly } from "../../schema/displayDerivation.js";
11
+ import { stepReaches } from "../../schema/guards.js";
12
+ import {
13
+ EDITABLE_NOTE_KINDS,
14
+ storyKey,
15
+ storyNodeOf
16
+ } from "../../schema/stories.js";
17
+ import { documentOf } from "../editorDocument.js";
18
+ function kindOf(node, kinds) {
19
+ if (node.type.name !== "noteReference") return null;
20
+ return kinds.find((kind) => kind === node.attrs.kind) ?? null;
21
+ }
22
+ function referencesIn(doc, kinds) {
23
+ const found = [];
24
+ doc.descendants((node, pos) => {
25
+ const kind = kindOf(node, kinds);
26
+ const id = node.attrs.id;
27
+ if (kind !== null && typeof id === "string") {
28
+ found.push({ kind, key: storyKey(kind, id), pos });
29
+ }
30
+ return true;
31
+ });
32
+ return found;
33
+ }
34
+ function countsOf(references) {
35
+ return references.reduce(
36
+ (counts, { key }) => counts.set(key, (counts.get(key) ?? 0) + 1),
37
+ /* @__PURE__ */ new Map()
38
+ );
39
+ }
40
+ function reachesReferences(tr, kinds) {
41
+ if (!tr.docChanged || tr.getMeta(displayOnly) === true) return false;
42
+ return tr.steps.some(
43
+ (step, index) => !(step instanceof DocAttrStep) && stepReaches(
44
+ step,
45
+ tr.docs[index],
46
+ tr.docs[index + 1] ?? tr.doc,
47
+ (node) => kindOf(node, kinds) !== null
48
+ )
49
+ );
50
+ }
51
+ function copiedReferences(transactions, before, after) {
52
+ const found = countsOf(before);
53
+ const left = countsOf(after);
54
+ const mapping = new Mapping(transactions.flatMap((tr) => tr.mapping.maps));
55
+ const carried = new Set(
56
+ before.flatMap(({ key, pos }) => {
57
+ const mapped = mapping.mapResult(pos, 1);
58
+ return mapped.deletedAfter ? [] : [`${key}@${mapped.pos}`];
59
+ })
60
+ );
61
+ const wasCarried = ({ key, pos }) => carried.has(`${key}@${pos}`);
62
+ return [...left].flatMap(([key, count]) => {
63
+ const kept = found.get(key) ?? 0;
64
+ if (kept === 0 || count <= kept) return [];
65
+ const standing = after.filter((reference) => reference.key === key);
66
+ return [
67
+ ...standing.filter(wasCarried),
68
+ ...standing.filter((reference) => !wasCarried(reference))
69
+ ].slice(kept);
70
+ });
71
+ }
72
+ function settled(transactions, oldState, newState, kinds) {
73
+ const document = documentOf(newState);
74
+ const isNote = (key) => !document.specialNotes.has(key) && storyNodeOf(newState.doc, key) !== null;
75
+ const before = referencesIn(oldState.doc, kinds);
76
+ const after = referencesIn(newState.doc, kinds);
77
+ const left = countsOf(after);
78
+ const gone = [...countsOf(before).keys()].filter(
79
+ (key) => !left.has(key) && isNote(key)
80
+ );
81
+ const copies = copiedReferences(transactions, before, after).filter(({ key }) => isNote(key)).sort((a, b) => a.pos - b.pos);
82
+ if (gone.length === 0 && copies.length === 0) return null;
83
+ const tr = withoutStories(newState.tr, gone);
84
+ const taken = /* @__PURE__ */ new Map();
85
+ for (const copy of copies) {
86
+ const story = storyNodeOf(newState.doc, copy.key);
87
+ if (story === null) continue;
88
+ const ids = taken.get(copy.kind) ?? takenNoteIds(newState.doc, copy.kind, document.reservedNoteKeys);
89
+ taken.set(copy.kind, ids);
90
+ const id = nextNoteId(ids);
91
+ ids.add(id);
92
+ tr.setNodeAttribute(copy.pos, "id", id).setNodeAttribute(
93
+ copy.pos,
94
+ "referenceXml",
95
+ null
96
+ );
97
+ setStory(
98
+ tr,
99
+ storyKey(copy.kind, id),
100
+ copiedNoteStory(story, document.session)
101
+ );
102
+ }
103
+ return tr;
104
+ }
105
+ function noteLifecycle(kinds = EDITABLE_NOTE_KINDS) {
106
+ return new Plugin({
107
+ appendTransaction(transactions, oldState, newState) {
108
+ if (!transactions.some((tr) => reachesReferences(tr, kinds))) {
109
+ return null;
110
+ }
111
+ return settled(transactions, oldState, newState, kinds);
112
+ }
113
+ });
114
+ }
115
+ export {
116
+ noteLifecycle
117
+ };