@portone/docx-editor 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/assets/editor.png +0 -0
  4. package/dist/DocxEditor.d.ts +1 -1
  5. package/dist/DocxEditor.js +44 -25
  6. package/dist/docx/cloning.js +4 -9
  7. package/dist/docx/commentOnlyChange.d.ts +5 -3
  8. package/dist/docx/comments/grammar.d.ts +27 -10
  9. package/dist/docx/comments/grammar.js +18 -67
  10. package/dist/docx/comments/model.d.ts +5 -6
  11. package/dist/docx/comments/model.js +0 -9
  12. package/dist/docx/comments/parts.d.ts +16 -10
  13. package/dist/docx/comments/parts.js +65 -13
  14. package/dist/docx/comments/people.d.ts +11 -3
  15. package/dist/docx/comments/people.js +20 -6
  16. package/dist/docx/comments/policy.js +7 -1
  17. package/dist/docx/comments/reading.d.ts +4 -2
  18. package/dist/docx/comments/reading.js +1 -14
  19. package/dist/docx/comments/writing.js +65 -28
  20. package/dist/docx/exportDocx.js +10 -14
  21. package/dist/docx/exportRefs.d.ts +8 -3
  22. package/dist/docx/exportRefs.js +2 -1
  23. package/dist/docx/fidelity.js +24 -10
  24. package/dist/docx/fields.d.ts +42 -0
  25. package/dist/docx/fields.js +76 -0
  26. package/dist/docx/formatting/styles.d.ts +0 -2
  27. package/dist/docx/formatting/styles.js +2 -15
  28. package/dist/docx/headersFooters.d.ts +89 -20
  29. package/dist/docx/headersFooters.js +163 -172
  30. package/dist/docx/identities.js +17 -26
  31. package/dist/docx/importDocx.d.ts +1 -1
  32. package/dist/docx/importDocx.js +100 -48
  33. package/dist/docx/importParagraph.d.ts +18 -5
  34. package/dist/docx/importParagraph.js +75 -128
  35. package/dist/docx/importPolicy.d.ts +70 -0
  36. package/dist/docx/importPolicy.js +195 -0
  37. package/dist/docx/importPreserved.d.ts +30 -0
  38. package/dist/docx/importPreserved.js +54 -0
  39. package/dist/docx/importTable.js +56 -12
  40. package/dist/docx/invariants.js +16 -18
  41. package/dist/docx/notes.d.ts +6 -1
  42. package/dist/docx/notes.js +6 -16
  43. package/dist/docx/pageGeometry.d.ts +5 -14
  44. package/dist/docx/pageGeometry.js +0 -12
  45. package/dist/docx/protectionPolicy.d.ts +13 -5
  46. package/dist/docx/protectionPolicy.js +32 -22
  47. package/dist/docx/scan.d.ts +18 -8
  48. package/dist/docx/scan.js +17 -11
  49. package/dist/docx/sections.d.ts +126 -0
  50. package/dist/docx/sections.js +207 -0
  51. package/dist/docx/serializeBlock.d.ts +13 -4
  52. package/dist/docx/serializeBlock.js +16 -18
  53. package/dist/docx/serializeParagraph.d.ts +9 -4
  54. package/dist/docx/serializeParagraph.js +23 -58
  55. package/dist/docx/serializePreserved.d.ts +14 -0
  56. package/dist/docx/serializePreserved.js +24 -0
  57. package/dist/docx/serializeStory.d.ts +17 -0
  58. package/dist/docx/serializeStory.js +17 -0
  59. package/dist/docx/serializeTable.js +10 -13
  60. package/dist/docx/session.d.ts +23 -6
  61. package/dist/docx/session.js +26 -5
  62. package/dist/docx/story.d.ts +140 -0
  63. package/dist/docx/story.js +237 -0
  64. package/dist/docx/storyProjection.js +6 -1
  65. package/dist/docx/tableTemplate.d.ts +25 -0
  66. package/dist/docx/tableTemplate.js +30 -16
  67. package/dist/docx/wrappers.d.ts +67 -0
  68. package/dist/docx/wrappers.js +117 -0
  69. package/dist/editor/clipboard/blockReaders.d.ts +74 -0
  70. package/dist/editor/clipboard/blockReaders.js +193 -0
  71. package/dist/editor/clipboard/htmlReader.d.ts +26 -0
  72. package/dist/editor/clipboard/htmlReader.js +376 -0
  73. package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
  74. package/dist/editor/clipboard/inlineFormatting.js +1 -7
  75. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  76. package/dist/editor/clipboard/internalChannel.js +58 -0
  77. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  78. package/dist/editor/clipboard/normalizers.js +199 -0
  79. package/dist/editor/clipboard/parser.d.ts +34 -0
  80. package/dist/editor/clipboard/parser.js +58 -0
  81. package/dist/editor/clipboard/plugin.d.ts +21 -0
  82. package/dist/editor/clipboard/plugin.js +188 -0
  83. package/dist/editor/clipboard/readContext.d.ts +34 -0
  84. package/dist/editor/clipboard/readContext.js +25 -0
  85. package/dist/editor/clipboard/readers.d.ts +33 -0
  86. package/dist/editor/clipboard/readers.js +37 -0
  87. package/dist/editor/clipboard/source.d.ts +12 -0
  88. package/dist/editor/clipboard/source.js +21 -0
  89. package/dist/editor/commands/comments/editing.d.ts +26 -6
  90. package/dist/editor/commands/comments/editing.js +177 -106
  91. package/dist/editor/commands/comments/model.d.ts +34 -17
  92. package/dist/editor/commands/comments/model.js +11 -0
  93. package/dist/editor/commands/comments/reading.d.ts +5 -3
  94. package/dist/editor/commands/comments/reading.js +8 -58
  95. package/dist/editor/commands/index.d.ts +2 -2
  96. package/dist/editor/commands/index.js +2 -0
  97. package/dist/editor/commands/linkCommands.js +20 -5
  98. package/dist/editor/commands/listCommands.d.ts +6 -0
  99. package/dist/editor/commands/listCommands.js +3 -2
  100. package/dist/editor/commands/lockCommands.js +59 -16
  101. package/dist/editor/commands/noteQueries.d.ts +22 -5
  102. package/dist/editor/commands/noteQueries.js +34 -6
  103. package/dist/editor/commands/paragraphCommands.js +2 -2
  104. package/dist/editor/createEditor.js +13 -3
  105. package/dist/editor/documentStyles.d.ts +26 -4
  106. package/dist/editor/documentStyles.js +9 -4
  107. package/dist/editor/imageFiles.d.ts +2 -2
  108. package/dist/editor/imageFiles.js +2 -0
  109. package/dist/editor/insertTable.d.ts +1 -1
  110. package/dist/editor/insertTable.js +2 -2
  111. package/dist/editor/plainText.d.ts +12 -0
  112. package/dist/editor/plainText.js +2 -1
  113. package/dist/editor/plugins/columnResize.js +6 -3
  114. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  115. package/dist/editor/plugins/commentComposer.js +46 -0
  116. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  117. package/dist/editor/plugins/commentDecorations.js +89 -15
  118. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  119. package/dist/editor/plugins/documentProjection.js +22 -0
  120. package/dist/editor/plugins/imagePaste.js +15 -8
  121. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  122. package/dist/editor/plugins/linkPanel.js +13 -27
  123. package/dist/editor/plugins/panelState.d.ts +67 -0
  124. package/dist/editor/plugins/panelState.js +54 -0
  125. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  126. package/dist/editor/plugins/tableContextMenu.js +30 -47
  127. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  128. package/dist/editor/plugins/textContextMenu.js +30 -47
  129. package/dist/ooxml/fragment.d.ts +11 -0
  130. package/dist/ooxml/fragment.js +14 -4
  131. package/dist/ooxml/image.d.ts +3 -4
  132. package/dist/ooxml/image.js +50 -5
  133. package/dist/ooxml/names.d.ts +6 -0
  134. package/dist/ooxml/names.js +2 -0
  135. package/dist/ooxml/props.d.ts +10 -0
  136. package/dist/ooxml/props.js +28 -4
  137. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  138. package/dist/ooxml/rangeMarkers.js +24 -0
  139. package/dist/page/PageGuides.d.ts +7 -3
  140. package/dist/page/PageGuides.js +13 -24
  141. package/dist/page/pageLayout.d.ts +64 -5
  142. package/dist/page/pageLayout.js +90 -29
  143. package/dist/page/usePageLayout.d.ts +14 -4
  144. package/dist/page/usePageLayout.js +19 -20
  145. package/dist/schema/attrRoles.js +46 -23
  146. package/dist/schema/clipboard.d.ts +72 -0
  147. package/dist/schema/clipboard.js +288 -0
  148. package/dist/schema/docxSchema.d.ts +9 -1
  149. package/dist/schema/docxSchema.js +217 -165
  150. package/dist/schema/editGuard.d.ts +1 -1
  151. package/dist/schema/guards.js +2 -2
  152. package/dist/schema/index.d.ts +1 -0
  153. package/dist/schema/locks.d.ts +3 -0
  154. package/dist/schema/locks.js +29 -12
  155. package/dist/schema/preservedFragments.d.ts +12 -0
  156. package/dist/schema/preservedFragments.js +38 -0
  157. package/dist/schema/preservedGuards.d.ts +9 -7
  158. package/dist/schema/preservedGuards.js +112 -11
  159. package/dist/schema/protection.d.ts +34 -5
  160. package/dist/schema/protection.js +42 -14
  161. package/dist/schema/rendering.d.ts +14 -0
  162. package/dist/schema/rendering.js +27 -12
  163. package/dist/schema/stories.d.ts +49 -0
  164. package/dist/schema/stories.js +78 -0
  165. package/dist/schema/wrappers.d.ts +58 -0
  166. package/dist/schema/wrappers.js +77 -0
  167. package/dist/styles/classNames.d.ts +3 -5
  168. package/dist/styles/classNames.js +3 -5
  169. package/dist/styles.css +74 -12
  170. package/dist/table/resize.d.ts +5 -3
  171. package/dist/ui/CommentsPanel.d.ts +1 -3
  172. package/dist/ui/CommentsPanel.js +24 -15
  173. package/dist/ui/NotesPanel.js +2 -2
  174. package/dist/ui/TextMenu.d.ts +1 -3
  175. package/dist/ui/TextMenu.js +10 -6
  176. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  177. package/dist/ui/comments/CommentComposer.js +26 -3
  178. package/package.json +5 -4
  179. package/dist/editor/externalClipboard.d.ts +0 -22
  180. package/dist/editor/externalClipboard.js +0 -486
@@ -10,6 +10,7 @@ import {
10
10
  pxToEmu,
11
11
  toImageSrc
12
12
  } from "../ooxml/image.js";
13
+ import { plainTextPaste } from "./clipboard/parser.js";
13
14
  import { documentBodyWidthPx } from "./documentStyles.js";
14
15
  import { insertImage } from "./insertImage.js";
15
16
  import { moveCaretToDrop } from "./plugins/dropCaret.js";
@@ -140,6 +141,7 @@ function imageFiles() {
140
141
  return new Plugin({
141
142
  props: {
142
143
  handlePaste(view, event) {
144
+ if (plainTextPaste(view, event)) return false;
143
145
  const files = imageFilesIn(event.clipboardData);
144
146
  if (files.length === 0) return false;
145
147
  void insertImageFiles(view, files);
@@ -13,6 +13,6 @@ export interface TableSize {
13
13
  }
14
14
  /**
15
15
  * Inserts a table of empty cells and moves the caret into the first cell.
16
- * The table is as wide as one line of body text on the paper the open document names
16
+ * The table is as wide as one line of body text on the paper of the section it goes into
17
17
  */
18
18
  export declare function insertTable({ rows, columns }: TableSize): Command;
@@ -5,7 +5,7 @@ import {
5
5
  } from "prosemirror-state";
6
6
  import { createTableNode, isTableSide } from "../docx/tableTemplate.js";
7
7
  import { editShut } from "../schema/guards.js";
8
- import { documentGeometry } from "./documentStyles.js";
8
+ import { sectionGeometryAt } from "./documentStyles.js";
9
9
  function isInTable($pos) {
10
10
  for (let depth = $pos.depth; depth > 0; depth -= 1) {
11
11
  if ($pos.node(depth).type.spec.tableRole === "table") return true;
@@ -30,7 +30,7 @@ function insertTable({ rows, columns }) {
30
30
  const tr = state.tr.insert(
31
31
  at,
32
32
  Fragment.fromArray([
33
- createTableNode(rows, columns, documentGeometry(state)),
33
+ createTableNode(rows, columns, sectionGeometryAt(state, at)),
34
34
  state.schema.nodes.paragraph.create()
35
35
  ])
36
36
  );
@@ -1,3 +1,15 @@
1
1
  import type { EditorView } from "prosemirror-view";
2
+ /** A single character that becomes an element, or text that goes in as it is */
3
+ export type Piece = {
4
+ text: string;
5
+ } | {
6
+ node: "hardBreak" | "tab";
7
+ };
8
+ /**
9
+ * Splits the incoming text into the pieces to be put into the document.
10
+ * Control characters that carry meaning become elements, and the rest are stripped because they
11
+ * would make the file impossible to open.
12
+ */
13
+ export declare function toPieces(source: string): Piece[];
2
14
  export declare function insertPlainText(view: EditorView, source: string): void;
3
15
  export declare function insertPlainTextAt(view: EditorView, source: string, from: number, to: number): void;
@@ -46,5 +46,6 @@ function insertPlainTextAt(view, source, from, to) {
46
46
  }
47
47
  export {
48
48
  insertPlainText,
49
- insertPlainTextAt
49
+ insertPlainTextAt,
50
+ toPieces
50
51
  };
@@ -14,7 +14,7 @@ import {
14
14
  shiftedTableWidth,
15
15
  tableGridCols
16
16
  } from "../../table/widths.js";
17
- import { documentGeometry } from "../documentStyles.js";
17
+ import { sectionGeometryAt } from "../documentStyles.js";
18
18
  import { TableNodeView } from "../views/tableView.js";
19
19
  var EDGE_TOLERANCE_PX = 4;
20
20
  var FALLBACK_DXA_PER_PX = 15;
@@ -127,7 +127,7 @@ function columnResize() {
127
127
  col: current.target.col,
128
128
  width: current.width
129
129
  },
130
- documentGeometry(current.view.state)
130
+ sectionGeometryAt(current.view.state, current.target.tablePos)
131
131
  );
132
132
  if (tr) current.view.dispatch(tr);
133
133
  }
@@ -144,7 +144,10 @@ function columnResize() {
144
144
  drag = {
145
145
  view,
146
146
  target,
147
- maxTotal: maxGridTotal(target.table, documentGeometry(view.state)),
147
+ maxTotal: maxGridTotal(
148
+ target.table,
149
+ sectionGeometryAt(view.state, target.tablePos)
150
+ ),
148
151
  startX: event.clientX,
149
152
  dxaPerPx: dxaPerPx(target.gridCols, target.tableDom),
150
153
  cols,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Where the built-in comment composer stands: the stretch of text it was opened over, or nothing
3
+ * when it is shut.
4
+ *
5
+ * The stretch is the point of holding it here rather than in the component that draws the form.
6
+ * A comment is written about text the reader picked out, and writing it takes as long as it takes:
7
+ * the caret moves on, the page is scrolled, a word elsewhere is fixed. The stretch is moved along
8
+ * by every edit that happens meanwhile, so the comment lands on the text it was written for, and
9
+ * the form goes away by itself once that text is gone or the document stops taking comments.
10
+ *
11
+ * The commands here are the editor's own. A consumer opening its own composer from its own toolbar
12
+ * is a public surface this package has not settled yet, so nothing below is exported from
13
+ * `./commands`.
14
+ */
15
+ import type { Command, EditorState, Plugin } from "prosemirror-state";
16
+ import type { CommentRange } from "../commands/comments/model";
17
+ /**
18
+ * Opens the composer over the selected text. False where a comment could not go there anyway, so
19
+ * the menu entry that runs it is drawn dead rather than opening a form nothing would come of. It
20
+ * is the same question that entry is drawn from, asked of the same selection.
21
+ */
22
+ export declare const openCommentComposer: Command;
23
+ /** Closes it. Called on Cancel, on the comment being written, and on the panel being put away */
24
+ export declare const closeCommentComposer: Command;
25
+ export declare function isCommentComposerOpen(state: EditorState): boolean;
26
+ /** The stretch the open composer is writing about. Null when no composer stands */
27
+ export declare function commentComposerRange(state: EditorState): CommentRange | null;
28
+ export declare function commentComposer(): Plugin<CommentRange | null>;
@@ -0,0 +1,46 @@
1
+ // src/editor/plugins/commentComposer.ts
2
+ import { canAddComment } from "../commands/comments/editing.js";
3
+ import { panelPlugin } from "./panelState.js";
4
+ function isCommentRange(value) {
5
+ if (typeof value !== "object" || value === null) return false;
6
+ const { from, to } = value;
7
+ return typeof from === "number" && typeof to === "number";
8
+ }
9
+ var composer = panelPlugin({
10
+ name: "docxEditorCommentComposer",
11
+ isAnchor: isCommentRange,
12
+ // The stretch moves with the text around it. Its two ends are biased inwards, so text typed
13
+ // against either edge stays outside the comment and an edit that swallows the stretch leaves
14
+ // the two ends together, which is the form closing
15
+ onDocChange: (anchor, tr) => {
16
+ const from = tr.mapping.map(anchor.from, 1);
17
+ const to = tr.mapping.map(anchor.to, -1);
18
+ return to > from ? { from, to } : null;
19
+ },
20
+ // Asked after every transaction, which is what catches the mode being switched to read-only:
21
+ // that changes no text, so the mapping above never sees it. It is also what keeps the form from
22
+ // ever standing over a stretch that takes no comment, whoever opened it
23
+ closeWhen: (state, anchor) => !canAddComment(state, anchor)
24
+ });
25
+ var openCommentComposer = (state, dispatch) => {
26
+ if (!canAddComment(state)) return false;
27
+ const { from, to } = state.selection;
28
+ return composer.open({ from, to })(state, dispatch);
29
+ };
30
+ var closeCommentComposer = composer.close;
31
+ function isCommentComposerOpen(state) {
32
+ return composer.anchor(state) !== null;
33
+ }
34
+ function commentComposerRange(state) {
35
+ return composer.anchor(state);
36
+ }
37
+ function commentComposer() {
38
+ return composer.plugin;
39
+ }
40
+ export {
41
+ closeCommentComposer,
42
+ commentComposer,
43
+ commentComposerRange,
44
+ isCommentComposerOpen,
45
+ openCommentComposer
46
+ };
@@ -1,3 +1,30 @@
1
- /** Draws the text ranges identified by Word comment markers. */
2
- import { Plugin } from "prosemirror-state";
1
+ /**
2
+ * What the comments in a document come to, worked out once per edit: the threads in document
3
+ * order, the lookup a command asking after one of them uses, and the ranges drawn over the text
4
+ * each was written for.
5
+ *
6
+ * All three are read off the same walk. Which comments a document holds is decided by the document
7
+ * and nothing else, so it is a projection (`./documentProjection`) rather than something worked
8
+ * out again for every command asked, every decoration drawn and every render of the list beside
9
+ * the page.
10
+ */
11
+ import type { Plugin } from "prosemirror-state";
12
+ import { DecorationSet } from "prosemirror-view";
13
+ import { type DocumentComment } from "../commands/comments/model";
14
+ export interface DocumentComments {
15
+ comments: readonly DocumentComment[];
16
+ /**
17
+ * The comments by id. A document can hold the same id twice - a reference copied and pasted
18
+ * carries the one it was written with - and the first of them wins, which is the one a search
19
+ * through the list in document order arrived at.
20
+ */
21
+ byId: ReadonlyMap<string, DocumentComment>;
22
+ /** The stretch each comment was written for, drawn as it stood when the edit landed */
23
+ decorations: DecorationSet;
24
+ }
25
+ export declare const commentProjection: import("./documentProjection").DocumentProjection<DocumentComments>;
26
+ /**
27
+ * What a state built by `createEditor` registers: the comments it goes on to answer, and the
28
+ * ranges drawn over the text identified by the Word comment markers holding them.
29
+ */
3
30
  export declare function commentDecorations(): Plugin;
@@ -1,23 +1,97 @@
1
1
  // src/editor/plugins/commentDecorations.ts
2
- import { Plugin } from "prosemirror-state";
3
2
  import { Decoration, DecorationSet } from "prosemirror-view";
3
+ import { storyKey, storyOf, storyText } from "../../docx/story.js";
4
4
  import { editorClassNames } from "../../styles/classNames.js";
5
- import { documentComments } from "../commands/commentCommands.js";
6
- function commentDecorations() {
7
- return new Plugin({
8
- props: {
9
- decorations(state) {
10
- const decorations = documentComments(state).filter((comment) => comment.from < comment.to).map(
11
- (comment) => Decoration.inline(comment.from, comment.to, {
12
- class: editorClassNames.commentRange,
13
- "data-comment-id": comment.id
14
- })
15
- );
16
- return DecorationSet.create(state.doc, decorations);
17
- }
5
+ import {
6
+ repliesAttr,
7
+ stringAttr
8
+ } from "../commands/comments/model.js";
9
+ import { documentProjection } from "./documentProjection.js";
10
+ function markerPositions(doc) {
11
+ const starts = /* @__PURE__ */ new Map();
12
+ const ends = /* @__PURE__ */ new Map();
13
+ const references = [];
14
+ doc.descendants((node, pos) => {
15
+ const id = stringAttr(node.attrs.id);
16
+ if (id === null) return true;
17
+ if (node.type.name === "commentStart" && !starts.has(id)) {
18
+ starts.set(id, pos);
19
+ } else if (node.type.name === "commentEnd" && !ends.has(id)) {
20
+ ends.set(id, pos);
21
+ } else if (node.type.name === "commentReference") {
22
+ references.push({ node, pos });
18
23
  }
24
+ return true;
25
+ });
26
+ return { starts, ends, references };
27
+ }
28
+ function bodyText(doc, id) {
29
+ return storyText(storyOf(doc, storyKey("comment", id)));
30
+ }
31
+ function commentsIn(doc) {
32
+ const markers = markerPositions(doc);
33
+ return markers.references.map(({ node, pos }) => {
34
+ const id = stringAttr(node.attrs.id) ?? "";
35
+ const start = markers.starts.get(id);
36
+ const end = markers.ends.get(id);
37
+ const hasRange = start !== void 0 && end !== void 0 && start < end;
38
+ const point = start ?? end ?? pos;
39
+ return {
40
+ id,
41
+ author: stringAttr(node.attrs.author),
42
+ authorId: stringAttr(node.attrs.authorId),
43
+ initials: stringAttr(node.attrs.initials),
44
+ date: stringAttr(node.attrs.date),
45
+ text: bodyText(doc, id),
46
+ from: hasRange ? start + 1 : point,
47
+ to: hasRange ? end : point,
48
+ referencePos: pos,
49
+ resolved: node.attrs.resolved === true,
50
+ replies: repliesAttr(node.attrs.replies).map((reply) => ({
51
+ id: reply.id,
52
+ author: reply.author,
53
+ authorId: reply.authorId,
54
+ initials: reply.initials,
55
+ date: reply.date,
56
+ text: bodyText(doc, reply.id)
57
+ }))
58
+ };
19
59
  });
20
60
  }
61
+ function deriveComments(doc) {
62
+ const comments = commentsIn(doc);
63
+ const byId = /* @__PURE__ */ new Map();
64
+ for (const comment of comments) {
65
+ if (!byId.has(comment.id)) byId.set(comment.id, comment);
66
+ }
67
+ return {
68
+ comments,
69
+ byId,
70
+ decorations: DecorationSet.create(
71
+ doc,
72
+ comments.filter((comment) => comment.from < comment.to).map(
73
+ (comment) => Decoration.inline(comment.from, comment.to, {
74
+ class: editorClassNames.commentRange,
75
+ "data-comment-id": comment.id
76
+ })
77
+ )
78
+ )
79
+ };
80
+ }
81
+ var commentProjection = documentProjection(
82
+ "docxEditorComments",
83
+ deriveComments,
84
+ {
85
+ // The set the last edit derived. Written as a method so that `this` is the plugin holding it
86
+ decorations(state) {
87
+ return this.getState(state)?.decorations;
88
+ }
89
+ }
90
+ );
91
+ function commentDecorations() {
92
+ return commentProjection.plugin;
93
+ }
21
94
  export {
22
- commentDecorations
95
+ commentDecorations,
96
+ commentProjection
23
97
  };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * One convention for a value the whole document decides: the comments beside it, the notes under
3
+ * it, the list markers drawn over it.
4
+ *
5
+ * Such a value is worked out by walking the document, and what it is worked out from changes only
6
+ * when the document does. Held here, it is derived once per edit rather than once per read, and a
7
+ * caret moving, a menu opening or a component rendering again all read back the value the last
8
+ * edit left. What a caller writes is unchanged: `read` answers with a value either way.
9
+ *
10
+ * A projection derives from the document alone. Anything that also depends on the selection, the
11
+ * protection or another plugin belongs in that plugin, which is asked again after every
12
+ * transaction rather than after an edit alone.
13
+ */
14
+ import type { Node as PMNode } from "prosemirror-model";
15
+ import { type EditorState, Plugin, type PluginSpec } from "prosemirror-state";
16
+ export interface DocumentProjection<T> {
17
+ plugin: Plugin<T>;
18
+ /**
19
+ * The derived value. A state built without the plugin - one a consumer assembled itself, or one
20
+ * a test made out of a bare document - is answered by deriving on the spot, so a public reader
21
+ * standing on a projection keeps answering whoever asks it.
22
+ */
23
+ read(state: EditorState): T;
24
+ }
25
+ export declare function documentProjection<T>(name: string, derive: (doc: PMNode) => T,
26
+ /**
27
+ * The view props drawn from the derived value, which is how a projection that paints what it
28
+ * derived - comment ranges over the text they were written for - stays one plugin. `this` in
29
+ * such a prop is the plugin, so it reads the value back with `this.getState(state)`.
30
+ */
31
+ props?: PluginSpec<T>["props"]): DocumentProjection<T>;
@@ -0,0 +1,22 @@
1
+ // src/editor/plugins/documentProjection.ts
2
+ import {
3
+ Plugin,
4
+ PluginKey
5
+ } from "prosemirror-state";
6
+ function documentProjection(name, derive, props) {
7
+ const key = new PluginKey(name);
8
+ return {
9
+ plugin: new Plugin({
10
+ key,
11
+ state: {
12
+ init: (_config, state) => derive(state.doc),
13
+ apply: (tr, held) => tr.docChanged ? derive(tr.doc) : held
14
+ },
15
+ props
16
+ }),
17
+ read: (state) => key.getState(state) ?? derive(state.doc)
18
+ };
19
+ }
20
+ export {
21
+ documentProjection
22
+ };
@@ -3,12 +3,14 @@ import { isHistoryTransaction } from "prosemirror-history";
3
3
  import { Plugin, PluginKey } from "prosemirror-state";
4
4
  import { Decoration, DecorationSet } from "prosemirror-view";
5
5
  import { editShut, selectionIntents } from "../../schema/guards.js";
6
+ import { readHtmlSlice, withPastedContent } from "../clipboard/htmlReader.js";
6
7
  import {
7
8
  hasResolvableClipboardImages,
8
9
  resolveClipboardImages
9
10
  } from "../clipboard/images.js";
10
- import { documentBodyHeightPx, documentBodyWidthPx } from "../documentStyles.js";
11
- import { richHtmlSlice, withPastedContent } from "../externalClipboard.js";
11
+ import { plainTextPaste } from "../clipboard/parser.js";
12
+ import { readContextOf } from "../clipboard/readContext.js";
13
+ import { documentBodyWidthPx, sectionBodyHeightPx } from "../documentStyles.js";
12
14
  import { insertPlainTextAt } from "../plainText.js";
13
15
  var LOAD_TIMEOUT_MS = 15e3;
14
16
  var imagePasteKey = new PluginKey("docxImagePaste");
@@ -87,11 +89,13 @@ function imagePaste() {
87
89
  if (!range) return;
88
90
  removeMarker(task);
89
91
  if (task.view.isDestroyed) return;
90
- const content = resolved === null || resolved.images.size === 0 ? null : richHtmlSlice(
91
- task.view.state,
92
- task.view.dom.ownerDocument,
93
- resolved.source,
94
- resolved.images
92
+ const content = resolved === null || resolved.images.size === 0 ? null : readHtmlSlice(
93
+ readContextOf(
94
+ task.view.state,
95
+ task.view.dom.ownerDocument,
96
+ resolved.images
97
+ ),
98
+ resolved.source
95
99
  );
96
100
  if (content !== null) {
97
101
  task.view.dispatch(
@@ -225,6 +229,7 @@ function imagePaste() {
225
229
  props: {
226
230
  decorations: (state) => imagePasteKey.getState(state)?.decorations,
227
231
  handlePaste(view, event) {
232
+ if (plainTextPaste(view, event)) return false;
228
233
  if (selectionIntents(view.state.selection, "replace").some(
229
234
  (intent) => editShut(view.state, intent)
230
235
  )) {
@@ -256,8 +261,10 @@ function imagePaste() {
256
261
  view,
257
262
  html,
258
263
  text,
264
+ // One sheet is drawn at one width, the first section's, and a pasted image is fitted
265
+ // to it; how tall a page is, though, is the paper of the section it lands in
259
266
  maxWidthPx: documentBodyWidthPx(view.state),
260
- maxHeightPx: documentBodyHeightPx(view.state),
267
+ maxHeightPx: sectionBodyHeightPx(view.state, from),
261
268
  canceled: false,
262
269
  controller: null
263
270
  });
@@ -9,15 +9,19 @@
9
9
  * It closes on the document changing, which is what applying a link or taking one off does: the
10
10
  * panel is about the selection it opened over, and after an edit that selection has been answered.
11
11
  */
12
- import { type Command, type EditorState, Plugin } from "prosemirror-state";
12
+ import type { Command, EditorState, Plugin } from "prosemirror-state";
13
+ /** The panel is about the selection it opened over, so standing open is all there is to hold */
14
+ declare const OPEN = true;
13
15
  /** Whether the link panel is open */
14
16
  export declare function isLinkPanelOpen(state: EditorState): boolean;
15
17
  /**
16
18
  * Opens the panel. It reports false where a link could not go on anyway - a caret standing in no
17
19
  * link, a selection a lock leaves nothing of - so that Cmd+K there is a key this editor did not
18
- * take, and a control drawn from it is drawn dead.
20
+ * take, and a control drawn from it is drawn dead. A panel already open is one more such place:
21
+ * the button that opened it has nothing left to do while it stands.
19
22
  */
20
23
  export declare const openLinkPanel: Command;
21
24
  /** Closes it. Called on applying, on Escape, and on a click outside */
22
25
  export declare const closeLinkPanel: Command;
23
- export declare function linkPanel(): Plugin<boolean>;
26
+ export declare function linkPanel(): Plugin<typeof OPEN | null>;
27
+ export {};
@@ -1,35 +1,21 @@
1
1
  // src/editor/plugins/linkPanel.ts
2
- import {
3
- Plugin,
4
- PluginKey
5
- } from "prosemirror-state";
6
2
  import { canSetLink } from "../commands/linkCommands.js";
7
- var panelKey = new PluginKey("docxEditorLinkPanel");
3
+ import { panelPlugin } from "./panelState.js";
4
+ var OPEN = true;
5
+ var panel = panelPlugin({
6
+ name: "docxEditorLinkPanel",
7
+ isAnchor: (value) => value === OPEN,
8
+ // An edit answers the panel: the link went on, came off, or the text it was about moved
9
+ onDocChange: "close",
10
+ canOpen: (state) => canSetLink(state)
11
+ });
8
12
  function isLinkPanelOpen(state) {
9
- return panelKey.getState(state) === true;
13
+ return panel.anchor(state) === OPEN;
10
14
  }
11
- var openLinkPanel = (state, dispatch) => {
12
- if (isLinkPanelOpen(state) || !canSetLink(state)) return false;
13
- if (dispatch) dispatch(state.tr.setMeta(panelKey, true));
14
- return true;
15
- };
16
- var closeLinkPanel = (state, dispatch) => {
17
- if (!isLinkPanelOpen(state)) return false;
18
- if (dispatch) dispatch(state.tr.setMeta(panelKey, false));
19
- return true;
20
- };
15
+ var openLinkPanel = (state, dispatch) => !isLinkPanelOpen(state) && panel.open(OPEN)(state, dispatch);
16
+ var closeLinkPanel = panel.close;
21
17
  function linkPanel() {
22
- return new Plugin({
23
- key: panelKey,
24
- state: {
25
- init: () => false,
26
- apply: (tr, open) => {
27
- const meta = tr.getMeta(panelKey);
28
- if (typeof meta === "boolean") return meta;
29
- return tr.docChanged ? false : open;
30
- }
31
- }
32
- });
18
+ return panel.plugin;
33
19
  }
34
20
  export {
35
21
  closeLinkPanel,
@@ -0,0 +1,67 @@
1
+ /**
2
+ * One convention for every panel the editor opens over a spot in the document: a link panel, a
3
+ * right click menu, the comment composer.
4
+ *
5
+ * Each of them holds the same thing - where it stands, or nothing when it is shut - and answers the
6
+ * same three questions: open it here, close it, where does it stand. What differs between them is
7
+ * what "here" is and what an edit or a change of protection does to a panel already open, and those
8
+ * are the options below rather than four copies of a plugin.
9
+ *
10
+ * The anchor is whatever the panel is about: a point on the screen for a menu, a stretch of the
11
+ * document for the composer. It is read back out of the state as it was put in, so a panel that
12
+ * needs nothing but "open" carries an anchor that says just that.
13
+ *
14
+ * A panel that reads other plugins in `canOpen` or `closeWhen` must be registered after them:
15
+ * the state a plugin is handed while the transaction is being applied holds the fields of the
16
+ * plugins ahead of it and nothing of the ones behind it.
17
+ */
18
+ import { type Command, type EditorState, Plugin, type PluginSpec, type Transaction } from "prosemirror-state";
19
+ /**
20
+ * What a document change does to an open panel: shut it, or move the anchor along with the change.
21
+ * Answering null from the mapping shuts it, which is what a stretch the edit swallowed comes to.
22
+ */
23
+ export type PanelDocChange<Anchor> = "close" | ((anchor: Anchor, tr: Transaction, state: EditorState) => Anchor | null);
24
+ export interface PanelOptions<Anchor> {
25
+ /** The plugin key's name, which is what a state debugger and a duplicate registration report */
26
+ name: string;
27
+ /**
28
+ * Whether this is an anchor of this panel's. The opening goes through the transaction metadata,
29
+ * which anything may write, so what is read back is judged rather than trusted
30
+ */
31
+ isAnchor(value: unknown): value is Anchor;
32
+ onDocChange: PanelDocChange<Anchor>;
33
+ /** Whether the panel may open here. `open` reports false where it says no */
34
+ canOpen?(state: EditorState, anchor: Anchor): boolean;
35
+ /**
36
+ * Whether the panel must shut where the state now stands, asked after every transaction rather
37
+ * than after an edit alone: a protection switch changes no text (`./documentProtection`), so
38
+ * `onDocChange` never sees it, and a panel that only a commenter is offered has to go with it.
39
+ */
40
+ closeWhen?(state: EditorState, anchor: Anchor): boolean;
41
+ /**
42
+ * The view props the panel needs of its own, which is how a panel opened by a DOM event - a
43
+ * right click - stays one plugin. What that handler does with the event is the panel's business;
44
+ * all this factory asks is that the opening go through `opening` so the state stays this one.
45
+ */
46
+ props?: PluginSpec<Anchor | null>["props"];
47
+ }
48
+ export interface PanelPlugin<Anchor> {
49
+ plugin: Plugin<Anchor | null>;
50
+ /**
51
+ * Opens the panel over this anchor, or moves an open one there. False where `canOpen` says the
52
+ * panel has nothing to do here, so that a control drawn from it is drawn dead and a key bound to
53
+ * it is a key this editor did not take.
54
+ */
55
+ open(anchor: Anchor): Command;
56
+ /**
57
+ * The same opening recorded on a transaction already being built, for an opener that moves the
58
+ * selection in the same step. `canOpen` is not asked here: an opener holding the event that
59
+ * asked - a right click landing where the menu belongs - has decided that already.
60
+ */
61
+ opening(tr: Transaction, anchor: Anchor): Transaction;
62
+ /** Closes it. False where it stands closed already, so a key press falls through to what is behind it */
63
+ close: Command;
64
+ /** Where the panel stands. Null when it is shut, and for a state built without the plugin */
65
+ anchor(state: EditorState): Anchor | null;
66
+ }
67
+ export declare function panelPlugin<Anchor>({ name, isAnchor, onDocChange, canOpen, closeWhen, props, }: PanelOptions<Anchor>): PanelPlugin<Anchor>;
@@ -0,0 +1,54 @@
1
+ // src/editor/plugins/panelState.ts
2
+ import {
3
+ Plugin,
4
+ PluginKey
5
+ } from "prosemirror-state";
6
+ var CLOSE = /* @__PURE__ */ Symbol("panelClose");
7
+ function panelPlugin({
8
+ name,
9
+ isAnchor,
10
+ onDocChange,
11
+ canOpen,
12
+ closeWhen,
13
+ props
14
+ }) {
15
+ const key = new PluginKey(name);
16
+ const anchor = (state) => key.getState(state) ?? null;
17
+ const moved = (tr, held, next) => {
18
+ const meta = tr.getMeta(key);
19
+ if (meta === CLOSE) return null;
20
+ if (isAnchor(meta)) return meta;
21
+ if (held === null || !tr.docChanged) return held;
22
+ return onDocChange === "close" ? null : onDocChange(held, tr, next);
23
+ };
24
+ const opening = (tr, over) => tr.setMeta(key, over);
25
+ return {
26
+ plugin: new Plugin({
27
+ key,
28
+ state: {
29
+ init: () => null,
30
+ apply: (tr, held, _before, next) => {
31
+ const standing = moved(tr, held, next);
32
+ if (standing === null) return null;
33
+ return closeWhen?.(next, standing) ? null : standing;
34
+ }
35
+ },
36
+ props
37
+ }),
38
+ open: (over) => (state, dispatch) => {
39
+ if (canOpen && !canOpen(state, over)) return false;
40
+ dispatch?.(opening(state.tr, over));
41
+ return true;
42
+ },
43
+ opening,
44
+ close: (state, dispatch) => {
45
+ if (anchor(state) === null) return false;
46
+ dispatch?.(state.tr.setMeta(key, CLOSE));
47
+ return true;
48
+ },
49
+ anchor
50
+ };
51
+ }
52
+ export {
53
+ panelPlugin
54
+ };
@@ -8,7 +8,7 @@
8
8
  * selected text is taken first by the text menu (`editor/textContextMenu`), which stands ahead
9
9
  * of this one and hands back a click with nothing selected inside a cell.
10
10
  */
11
- import { type Command, type EditorState, Plugin } from "prosemirror-state";
11
+ import { type Command, type EditorState, type Plugin } from "prosemirror-state";
12
12
  /** Where the menu should stand (viewport coordinates) */
13
13
  export interface TableMenuAnchor {
14
14
  clientX: number;