@portone/docx-editor 0.1.1 → 0.2.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 (138) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +17 -5
  4. package/dist/DocxEditor.d.ts +34 -10
  5. package/dist/DocxEditor.js +69 -24
  6. package/dist/core.d.ts +2 -0
  7. package/dist/core.js +2 -0
  8. package/dist/docx/commentOnlyChange.d.ts +47 -0
  9. package/dist/docx/commentOnlyChange.js +162 -0
  10. package/dist/docx/comments/constants.d.ts +11 -3
  11. package/dist/docx/comments/constants.js +10 -3
  12. package/dist/docx/comments/contentTypes.d.ts +7 -0
  13. package/dist/docx/comments/contentTypes.js +38 -0
  14. package/dist/docx/comments/grammar.d.ts +80 -0
  15. package/dist/docx/comments/grammar.js +213 -0
  16. package/dist/docx/comments/model.d.ts +2 -0
  17. package/dist/docx/comments/model.js +3 -0
  18. package/dist/docx/comments/people.d.ts +39 -0
  19. package/dist/docx/comments/people.js +196 -0
  20. package/dist/docx/comments/reading.d.ts +18 -1
  21. package/dist/docx/comments/reading.js +33 -20
  22. package/dist/docx/comments/verifying.d.ts +44 -0
  23. package/dist/docx/comments/verifying.js +206 -0
  24. package/dist/docx/comments/writing.d.ts +4 -1
  25. package/dist/docx/comments/writing.js +84 -77
  26. package/dist/docx/exportDocx.js +2 -1
  27. package/dist/docx/formatting/direct.js +3 -6
  28. package/dist/docx/headersFooters.js +8 -10
  29. package/dist/docx/importParagraph.js +1 -0
  30. package/dist/docx/notes.js +3 -5
  31. package/dist/docx/paraProps.js +51 -46
  32. package/dist/docx/propsXml.d.ts +20 -2
  33. package/dist/docx/propsXml.js +72 -32
  34. package/dist/docx/relationships.js +8 -3
  35. package/dist/docx/runProps.js +34 -34
  36. package/dist/docx/sdt.js +16 -14
  37. package/dist/docx/serializeBlock.js +1 -2
  38. package/dist/docx/serializeParagraph.d.ts +2 -1
  39. package/dist/docx/serializeParagraph.js +25 -14
  40. package/dist/docx/serializeTable.js +38 -24
  41. package/dist/docx/storyProjection.d.ts +36 -0
  42. package/dist/docx/storyProjection.js +23 -0
  43. package/dist/docx/tableFormatting/editing.js +49 -73
  44. package/dist/docx/tableTemplate.js +24 -3
  45. package/dist/docx/theme.d.ts +0 -6
  46. package/dist/docx/theme.js +0 -8
  47. package/dist/editor/clipboard/images.js +17 -3
  48. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  49. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  50. package/dist/editor/commands/breakCommands.js +6 -2
  51. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  52. package/dist/editor/commands/canRunCommand.js +2 -2
  53. package/dist/editor/commands/comments/editing.js +9 -10
  54. package/dist/editor/commands/comments/model.d.ts +9 -0
  55. package/dist/editor/commands/comments/reading.d.ts +7 -0
  56. package/dist/editor/commands/comments/reading.js +14 -0
  57. package/dist/editor/commands/formatting/editing.js +6 -1
  58. package/dist/editor/commands/historyCommands.js +14 -5
  59. package/dist/editor/commands/index.d.ts +7 -1
  60. package/dist/editor/commands/index.js +4 -0
  61. package/dist/editor/commands/linkCommands.js +2 -0
  62. package/dist/editor/commands/lockCommands.js +12 -1
  63. package/dist/editor/commands/tabCommands.js +4 -2
  64. package/dist/editor/createEditor.d.ts +13 -3
  65. package/dist/editor/createEditor.js +23 -15
  66. package/dist/editor/externalClipboard.js +166 -4
  67. package/dist/editor/insertImage.js +6 -2
  68. package/dist/editor/insertTable.js +2 -1
  69. package/dist/editor/paragraphEdits.d.ts +2 -0
  70. package/dist/editor/paragraphEdits.js +2 -0
  71. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  72. package/dist/editor/plugins/documentProtection.js +44 -0
  73. package/dist/editor/plugins/imagePaste.js +4 -1
  74. package/dist/editor/plugins/keymap.d.ts +11 -1
  75. package/dist/editor/plugins/keymap.js +24 -4
  76. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  77. package/dist/editor/plugins/lockedContent.js +2 -2
  78. package/dist/editor/plugins/tableContextMenu.js +2 -1
  79. package/dist/editor/plugins/textContextMenu.js +6 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/numbering/listTemplate.js +32 -10
  82. package/dist/ooxml/element.d.ts +52 -0
  83. package/dist/ooxml/element.js +49 -0
  84. package/dist/ooxml/fragment.d.ts +53 -0
  85. package/dist/ooxml/fragment.js +76 -0
  86. package/dist/ooxml/image.js +4 -3
  87. package/dist/ooxml/names.d.ts +27 -0
  88. package/dist/ooxml/names.js +29 -0
  89. package/dist/ooxml/precedence.d.ts +31 -0
  90. package/dist/ooxml/precedence.js +51 -0
  91. package/dist/ooxml/xml.d.ts +31 -0
  92. package/dist/ooxml/xml.js +27 -0
  93. package/dist/page/blockKinds.d.ts +42 -0
  94. package/dist/page/blockKinds.js +0 -0
  95. package/dist/page/measureBlocks.d.ts +14 -1
  96. package/dist/page/measureBlocks.js +31 -11
  97. package/dist/page/pageDecorations.d.ts +16 -0
  98. package/dist/page/pageDecorations.js +1 -0
  99. package/dist/page/pageLayout.d.ts +8 -43
  100. package/dist/page/pageLayout.js +28 -51
  101. package/dist/page/tableMeasurements.d.ts +10 -2
  102. package/dist/page/tableMeasurements.js +20 -16
  103. package/dist/page/usePageLayout.d.ts +15 -0
  104. package/dist/page/usePageLayout.js +30 -2
  105. package/dist/schema/attrRoles.d.ts +35 -0
  106. package/dist/schema/attrRoles.js +106 -0
  107. package/dist/schema/docxSchema.d.ts +6 -0
  108. package/dist/schema/docxSchema.js +216 -100
  109. package/dist/schema/editGuard.d.ts +99 -0
  110. package/dist/schema/editGuard.js +43 -0
  111. package/dist/schema/guards.d.ts +68 -0
  112. package/dist/schema/guards.js +82 -0
  113. package/dist/schema/locks.d.ts +9 -8
  114. package/dist/schema/locks.js +32 -16
  115. package/dist/schema/preservedGuards.d.ts +30 -0
  116. package/dist/schema/preservedGuards.js +50 -0
  117. package/dist/schema/protection.d.ts +84 -0
  118. package/dist/schema/protection.js +171 -0
  119. package/dist/schema/protectionState.d.ts +20 -0
  120. package/dist/schema/protectionState.js +37 -0
  121. package/dist/schema/sourceEquality.d.ts +27 -0
  122. package/dist/schema/sourceEquality.js +65 -0
  123. package/dist/table/cellFormatting.js +5 -2
  124. package/dist/table/commands.d.ts +2 -2
  125. package/dist/table/commands.js +2 -2
  126. package/dist/table/index.d.ts +2 -2
  127. package/dist/table/merge.d.ts +2 -2
  128. package/dist/table/merge.js +3 -3
  129. package/dist/ui/CommentsPanel.d.ts +3 -3
  130. package/dist/ui/CommentsPanel.js +26 -18
  131. package/dist/ui/LinkCard.d.ts +1 -2
  132. package/dist/ui/LinkCard.js +3 -4
  133. package/dist/ui/TextMenu.js +15 -10
  134. package/package.json +5 -2
  135. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  136. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  137. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  138. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whether two nodes would be written back as the same XML.
3
+ *
4
+ * `Node.eq` answers a different question: whether they are the same node, display values and all.
5
+ * That is the right question inside an editing session, where both sides came from one import and
6
+ * a difference can only be an edit. It is the wrong one across a re-derivation, because the values
7
+ * worked out from the surroundings (`./attrRoles`) move without the document moving, and a block
8
+ * judged changed is a block rewritten, which costs it the markup the writer does not model.
9
+ *
10
+ * So the comparison is `Node.eq` with the display attrs left out, and nothing else left out: the
11
+ * type, the text, the marks and the source and session attrs all have to agree, and the recursion
12
+ * runs the same way, so a node that says the same thing in a different shape is still a change.
13
+ */
14
+ import { type Node as PMNode } from "prosemirror-model";
15
+ /**
16
+ * Whether the two would write the same XML: the same type, text, marks and source attrs, all the
17
+ * way down. What the editor works out for the screen is not compared.
18
+ */
19
+ export declare function sameSource(a: PMNode, b: PMNode): boolean;
20
+ /**
21
+ * The node with every worked-out attr back at the value the schema starts it on, on itself and on
22
+ * everything inside it.
23
+ *
24
+ * `sameSource` needs no such copy, and takes none. This is for a caller that has to hand a whole
25
+ * document to something comparing the ordinary way.
26
+ */
27
+ export declare function withoutDisplayAttrs(node: PMNode): PMNode;
@@ -0,0 +1,65 @@
1
+ // src/schema/sourceEquality.ts
2
+ import {
3
+ Fragment
4
+ } from "prosemirror-model";
5
+ import { displayAttrsOf } from "./attrRoles.js";
6
+ function sameValue(a, b) {
7
+ if (a === b) return true;
8
+ if (Array.isArray(a) || Array.isArray(b)) {
9
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, at) => sameValue(item, b[at]));
10
+ }
11
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
12
+ return false;
13
+ }
14
+ const left = Object.entries(a);
15
+ return left.length === Object.keys(b).length && left.every(
16
+ ([key, value]) => key in b && sameValue(value, b[key])
17
+ );
18
+ }
19
+ function sameAttrs(display, a, b) {
20
+ return Object.keys(a).filter((name) => !display.includes(name)).every((name) => sameValue(a[name], b[name]));
21
+ }
22
+ function sameMarks(a, b) {
23
+ return a.length === b.length && a.every((mark, at) => {
24
+ const other = b[at];
25
+ return other !== void 0 && mark.type === other.type && sameAttrs(displayAttrsOf(mark.type), mark.attrs, other.attrs);
26
+ });
27
+ }
28
+ function sameSource(a, b) {
29
+ if (a === b) return true;
30
+ if (a.type !== b.type || !sameMarks(a.marks, b.marks) || !sameAttrs(displayAttrsOf(a.type), a.attrs, b.attrs)) {
31
+ return false;
32
+ }
33
+ if (a.isText) return a.text === b.text;
34
+ if (a.childCount !== b.childCount) return false;
35
+ for (let at = 0; at < a.childCount; at += 1) {
36
+ if (!sameSource(a.child(at), b.child(at))) return false;
37
+ }
38
+ return true;
39
+ }
40
+ function withoutDisplay(type, attrs) {
41
+ return Object.fromEntries(
42
+ Object.entries(attrs).map(
43
+ ([name, value]) => displayAttrsOf(type).includes(name) ? [name, type.spec.attrs?.[name]?.default ?? null] : [name, value]
44
+ )
45
+ );
46
+ }
47
+ function withoutDisplayMarkAttrs(mark) {
48
+ return displayAttrsOf(mark.type).length === 0 ? mark : mark.type.create(withoutDisplay(mark.type, mark.attrs));
49
+ }
50
+ var childrenOf = (node) => Array.from({ length: node.childCount }, (_, at) => node.child(at));
51
+ function withoutDisplayAttrs(node) {
52
+ const marks = node.marks.map(withoutDisplayMarkAttrs);
53
+ if (node.isText) return node.mark(marks);
54
+ return node.type.create(
55
+ withoutDisplay(node.type, node.attrs),
56
+ Fragment.fromArray(
57
+ childrenOf(node).map((child) => withoutDisplayAttrs(child))
58
+ ),
59
+ marks
60
+ );
61
+ }
62
+ export {
63
+ sameSource,
64
+ withoutDisplayAttrs
65
+ };
@@ -8,7 +8,9 @@ import {
8
8
  NO_FILL,
9
9
  toCellFormat
10
10
  } from "../model/format.js";
11
- import { isLockedCell, transactionAllowed } from "../schema/locks.js";
11
+ import { transactionAllowed } from "../schema/guards.js";
12
+ import { isLockedCell } from "../schema/locks.js";
13
+ import { editsShut } from "../schema/protectionState.js";
12
14
  import { cellDefaultsAt, tableCellSources } from "./gridBorders.js";
13
15
  function text(value) {
14
16
  return typeof value === "string" ? value : null;
@@ -59,7 +61,7 @@ function cellFormatCommand(edit) {
59
61
  const changes = planChanges(rect, edit);
60
62
  if (changes.length === 0) return false;
61
63
  const transaction = applyChanges(state.tr, rect.tableStart, changes);
62
- if (!transactionAllowed(transaction, state.doc)) return false;
64
+ if (!transactionAllowed(transaction, state)) return false;
63
65
  dispatch?.(transaction);
64
66
  return true;
65
67
  };
@@ -118,6 +120,7 @@ function cellFormats(state) {
118
120
  return selectedCells(state).map((cell) => toCellFormat(cell.attrs.format));
119
121
  }
120
122
  function canSetCellFormatting(state) {
123
+ if (editsShut(state)) return false;
121
124
  const cells = selectedCells(state);
122
125
  return cells.length > 0 && cells.every((cell) => !isLockedCell(cell));
123
126
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Building a transaction and running it are kept apart.
3
3
  * Following the ProseMirror convention, calling a command without `dispatch` only
4
- * reports whether it can run right now, and that answer takes in the lock guard
5
- * (`schema/locks`), so no caller has to ask about locks separately.
4
+ * reports whether it can run right now, and that answer takes in every guard an edit
5
+ * is judged by (`schema/guards`), so no caller has to ask about them separately.
6
6
  */
7
7
  import type { EditorState, Transaction } from "prosemirror-state";
8
8
  export type TableCommand = (state: EditorState, dispatch?: (tr: Transaction) => void) => boolean;
@@ -3,7 +3,7 @@ import {
3
3
  deleteTable as pmDeleteTable,
4
4
  isInTable as pmIsInTable
5
5
  } from "prosemirror-tables";
6
- import { transactionAllowed } from "../schema/locks.js";
6
+ import { transactionAllowed } from "../schema/guards.js";
7
7
  import {
8
8
  buildAddColumnAfterTransaction,
9
9
  buildAddColumnBeforeTransaction,
@@ -21,7 +21,7 @@ function isInTable(state) {
21
21
  function toCommand(build) {
22
22
  return (state, dispatch) => {
23
23
  const tr = build(state);
24
- if (!tr || !transactionAllowed(tr, state.doc)) return false;
24
+ if (!tr || !transactionAllowed(tr, state)) return false;
25
25
  dispatch?.(tr);
26
26
  return true;
27
27
  };
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Only what the toolbar needs is exported. Following the ProseMirror convention,
5
5
  * calling a command without `dispatch` only reports whether it can run right now, and that answer
6
- * takes in the lock: a structural edit that would carry a locked cell off reports that it does not
7
- * apply, rather than being refused after the click (`schema/locks`).
6
+ * takes in every guard an edit is judged by (`schema/guards`): a structural edit that would carry a
7
+ * locked cell off reports that it does not apply, rather than being refused after the click.
8
8
  *
9
9
  * Everything here works on a table already in the document; putting a new one into the body is
10
10
  * `insertTable` in `./commands`, which reads the page geometry the editor holds.
@@ -6,7 +6,7 @@ import type { EditorState, Transaction } from "prosemirror-state";
6
6
  /**
7
7
  * Whether several cells can be merged into one.
8
8
  * The selection has to be a rectangle spanning more than one cell, no merged cell may stick out
9
- * past that rectangle, and the lock guard has to let the merge through: a locked cell may not be
9
+ * past that rectangle, and the guards have to let the merge through: a locked cell may not be
10
10
  * swallowed by another (`schema/locks`).
11
11
  *
12
12
  * The two queries are defined from the very transaction the commands run, so they cannot drift
@@ -16,7 +16,7 @@ import type { EditorState, Transaction } from "prosemirror-state";
16
16
  export declare function canMergeCells(state: EditorState): boolean;
17
17
  /**
18
18
  * Whether a split is possible. The cursor has to sit inside a cell that is merged horizontally or
19
- * vertically, and the lock guard has to let the split through: the cells a split makes would each
19
+ * vertically, and the guards have to let the split through: the cells a split makes would each
20
20
  * carry the original's lock, which is a lock planted in places it was never put (`schema/locks`).
21
21
  */
22
22
  export declare function canSplitCell(state: EditorState): boolean;
@@ -11,7 +11,7 @@ import {
11
11
  widthNumber,
12
12
  withWidthNumber
13
13
  } from "../model/format.js";
14
- import { transactionAllowed } from "../schema/locks.js";
14
+ import { transactionAllowed } from "../schema/guards.js";
15
15
  import { inheritCellAttrs } from "./format.js";
16
16
  import { cellWidthForGridCol, gridSpanWidth, tableGridCols } from "./widths.js";
17
17
  function tableAfter(tr, tableStart) {
@@ -26,11 +26,11 @@ function cellAt(rect, row, col) {
26
26
  }
27
27
  function canMergeCells(state) {
28
28
  const tr = buildMergeCellsTransaction(state);
29
- return tr !== null && transactionAllowed(tr, state.doc);
29
+ return tr !== null && transactionAllowed(tr, state);
30
30
  }
31
31
  function canSplitCell(state) {
32
32
  const tr = buildSplitCellTransaction(state);
33
- return tr !== null && transactionAllowed(tr, state.doc);
33
+ return tr !== null && transactionAllowed(tr, state);
34
34
  }
35
35
  function coveredWidthSum(rect, type) {
36
36
  let sum = 0;
@@ -5,11 +5,11 @@ import { type CommentAuthor } from "../editor/commands/commentCommands";
5
5
  export interface CommentsPanelProps {
6
6
  view: EditorView;
7
7
  state: EditorState;
8
- readOnly: boolean;
9
8
  composerOpen: boolean;
10
- author: CommentAuthor;
9
+ /** Whose comments the composers write. Null for a reader, who is offered none */
10
+ author: CommentAuthor | null;
11
11
  closeComposer: () => void;
12
12
  scrollContainer: HTMLElement | null;
13
13
  allCommentsOpen: boolean;
14
14
  }
15
- export declare function CommentsPanel({ view, state, readOnly, composerOpen, author, closeComposer, scrollContainer, allCommentsOpen, }: CommentsPanelProps): ReactElement;
15
+ export declare function CommentsPanel({ view, state, composerOpen, author, closeComposer, scrollContainer, allCommentsOpen, }: CommentsPanelProps): ReactElement;
@@ -12,6 +12,8 @@ import {
12
12
  updateComment,
13
13
  updateCommentReply
14
14
  } from "../editor/commands/commentCommands.js";
15
+ import { commentOwned } from "../schema/protection.js";
16
+ import { protectionOf } from "../schema/protectionState.js";
15
17
  import { editorClassNames } from "../styles/classNames.js";
16
18
  import { CommentComposer, FocusedTextarea } from "./comments/CommentComposer.js";
17
19
  import {
@@ -41,7 +43,6 @@ function sameTarget(left, right) {
41
43
  function CommentsPanel({
42
44
  view,
43
45
  state,
44
- readOnly,
45
46
  composerOpen,
46
47
  author,
47
48
  closeComposer,
@@ -49,6 +50,9 @@ function CommentsPanel({
49
50
  allCommentsOpen
50
51
  }) {
51
52
  const comments = useMemo(() => documentComments(state), [state.doc]);
53
+ const rules = protectionOf(state);
54
+ const writer = author != null && rules.protection !== "readOnly" ? author : null;
55
+ const owned = (authorId) => writer !== null && commentOwned(rules, authorId);
52
56
  const panel = useRef(null);
53
57
  const [editing, setEditing] = useState(null);
54
58
  const [draft, setDraft] = useState("");
@@ -108,7 +112,7 @@ function CommentsPanel({
108
112
  className: editorClassNames.commentsCanvas,
109
113
  style: { height: allCommentsOpen ? void 0 : `${canvasHeight}px` },
110
114
  children: [
111
- composerOpen && !readOnly && /* @__PURE__ */ jsx(
115
+ composerOpen && writer !== null && /* @__PURE__ */ jsx(
112
116
  "div",
113
117
  {
114
118
  className: editorClassNames.commentPosition,
@@ -119,7 +123,7 @@ function CommentsPanel({
119
123
  children: /* @__PURE__ */ jsx(
120
124
  CommentComposer,
121
125
  {
122
- author,
126
+ author: writer,
123
127
  label: "Comment text",
124
128
  submitLabel: "Comment",
125
129
  onClose: closeComposer,
@@ -127,8 +131,9 @@ function CommentsPanel({
127
131
  view,
128
132
  addComment({
129
133
  text,
130
- author: author.name,
131
- initials: author.initials
134
+ author: writer.name,
135
+ authorId: writer.id,
136
+ initials: writer.initials
132
137
  })
133
138
  )
134
139
  }
@@ -137,7 +142,8 @@ function CommentsPanel({
137
142
  ),
138
143
  visible.map((comment) => {
139
144
  const rootTarget = { commentId: comment.id, replyId: null };
140
- const rootEditing = !readOnly && sameTarget(editing, rootTarget);
145
+ const rootOwned = owned(comment.authorId);
146
+ const rootEditing = rootOwned && sameTarget(editing, rootTarget);
141
147
  return /* @__PURE__ */ jsx(
142
148
  "article",
143
149
  {
@@ -160,8 +166,8 @@ function CommentsPanel({
160
166
  ]
161
167
  }
162
168
  ),
163
- !readOnly && !rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentIconActions, children: [
164
- !comment.resolved && /* @__PURE__ */ jsx(
169
+ writer !== null && !rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentIconActions, children: [
170
+ rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
165
171
  "button",
166
172
  {
167
173
  type: "button",
@@ -190,7 +196,7 @@ function CommentsPanel({
190
196
  children: comment.resolved ? /* @__PURE__ */ jsx(RotateCcw, { size: 16, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": "true" })
191
197
  }
192
198
  ),
193
- !comment.resolved && /* @__PURE__ */ jsx(
199
+ rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
194
200
  "button",
195
201
  {
196
202
  type: "button",
@@ -209,7 +215,7 @@ function CommentsPanel({
209
215
  value: draft,
210
216
  onChange: setDraft
211
217
  }
212
- ) : readOnly || comment.resolved ? /* @__PURE__ */ jsx("p", { className: editorClassNames.commentBody, children: comment.text }) : /* @__PURE__ */ jsx(
218
+ ) : writer === null || comment.resolved ? /* @__PURE__ */ jsx("p", { className: editorClassNames.commentBody, children: comment.text }) : /* @__PURE__ */ jsx(
213
219
  "button",
214
220
  {
215
221
  type: "button",
@@ -222,7 +228,7 @@ function CommentsPanel({
222
228
  children: comment.text
223
229
  }
224
230
  ),
225
- !readOnly && /* @__PURE__ */ jsx("div", { className: editorClassNames.commentActions, children: rootEditing ? /* @__PURE__ */ jsxs(Fragment, { children: [
231
+ rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentActions, children: [
226
232
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setEditing(null), children: "Cancel" }),
227
233
  /* @__PURE__ */ jsx(
228
234
  "button",
@@ -234,13 +240,14 @@ function CommentsPanel({
234
240
  children: "Save"
235
241
  }
236
242
  )
237
- ] }) : null }),
243
+ ] }),
238
244
  comment.replies.map((reply) => {
239
245
  const target = {
240
246
  commentId: comment.id,
241
247
  replyId: reply.id
242
248
  };
243
- const isEditing = !readOnly && sameTarget(editing, target);
249
+ const replyOwned = owned(reply.authorId);
250
+ const isEditing = replyOwned && sameTarget(editing, target);
244
251
  return /* @__PURE__ */ jsxs(
245
252
  "div",
246
253
  {
@@ -258,7 +265,7 @@ function CommentsPanel({
258
265
  onChange: setDraft
259
266
  }
260
267
  ) : /* @__PURE__ */ jsx("p", { className: editorClassNames.commentBody, children: reply.text }),
261
- !readOnly && !comment.resolved && /* @__PURE__ */ jsx("div", { className: editorClassNames.commentActions, children: isEditing ? /* @__PURE__ */ jsxs(Fragment, { children: [
268
+ replyOwned && !comment.resolved && /* @__PURE__ */ jsx("div", { className: editorClassNames.commentActions, children: isEditing ? /* @__PURE__ */ jsxs(Fragment, { children: [
262
269
  /* @__PURE__ */ jsx(
263
270
  "button",
264
271
  {
@@ -313,10 +320,10 @@ function CommentsPanel({
313
320
  reply.id
314
321
  );
315
322
  }),
316
- replying === comment.id && !readOnly && !comment.resolved && /* @__PURE__ */ jsx(
323
+ replying === comment.id && writer !== null && !comment.resolved && /* @__PURE__ */ jsx(
317
324
  CommentComposer,
318
325
  {
319
- author,
326
+ author: writer,
320
327
  label: "Reply text",
321
328
  submitLabel: "Reply",
322
329
  onClose: () => setReplying(null),
@@ -324,8 +331,9 @@ function CommentsPanel({
324
331
  view,
325
332
  addCommentReply(comment.id, {
326
333
  text,
327
- author: author.name,
328
- initials: author.initials
334
+ author: writer.name,
335
+ authorId: writer.id,
336
+ initials: writer.initials
329
337
  })
330
338
  )
331
339
  }
@@ -7,6 +7,5 @@ export interface LinkCardProps {
7
7
  view: EditorView;
8
8
  state: EditorState;
9
9
  link: ActiveLinkSpan;
10
- readOnly?: boolean;
11
10
  }
12
- export declare function LinkCard({ view, state, link, readOnly, }: LinkCardProps): ReactElement | null;
11
+ export declare function LinkCard({ view, state, link, }: LinkCardProps): ReactElement | null;
@@ -49,8 +49,7 @@ function useEscape(onEscape) {
49
49
  function LinkCard({
50
50
  view,
51
51
  state,
52
- link,
53
- readOnly = false
52
+ link
54
53
  }) {
55
54
  const box = useRef(null);
56
55
  const [hidden, setHidden] = useState(false);
@@ -94,7 +93,7 @@ function LinkCard({
94
93
  children: /* @__PURE__ */ jsx(ExternalLink, { size: ICON_SIZE, "aria-hidden": "true" })
95
94
  }
96
95
  ),
97
- !readOnly && openLinkPanel(state) && /* @__PURE__ */ jsx(
96
+ openLinkPanel(state) && /* @__PURE__ */ jsx(
98
97
  "button",
99
98
  {
100
99
  type: "button",
@@ -106,7 +105,7 @@ function LinkCard({
106
105
  children: /* @__PURE__ */ jsx(Pencil, { size: ICON_SIZE, "aria-hidden": "true" })
107
106
  }
108
107
  ),
109
- !readOnly && removeLink(state) && /* @__PURE__ */ jsx(
108
+ removeLink(state) && /* @__PURE__ */ jsx(
110
109
  "button",
111
110
  {
112
111
  type: "button",
@@ -21,6 +21,7 @@ import { insertClipboardData } from "../editor/externalClipboard.js";
21
21
  import {
22
22
  closeTextMenu
23
23
  } from "../editor/plugins/textContextMenu.js";
24
+ import { editsShut } from "../schema/protectionState.js";
24
25
  import { editorClassNames } from "../styles/classNames.js";
25
26
  import { usePanelAtPoint } from "./panelPlacement.js";
26
27
  import { commandRunner } from "./runCommand.js";
@@ -103,8 +104,16 @@ function TextMenu({
103
104
  const keys = useMenuKeyboard({ menu: box, onClose: close });
104
105
  const selected = !state.selection.empty;
105
106
  const shut = selectionTouchesLocked(state);
107
+ const bodyOpen = !editsShut(state);
108
+ const copy = {
109
+ label: "Copy",
110
+ icon: Copy,
111
+ hint: `${MOD}C`,
112
+ enabled: selected,
113
+ run: () => clipboardCommand(view, "copy")
114
+ };
106
115
  const groups = [
107
- [
116
+ bodyOpen ? [
108
117
  {
109
118
  label: "Cut",
110
119
  icon: Scissors,
@@ -112,13 +121,7 @@ function TextMenu({
112
121
  enabled: selected && !shut,
113
122
  run: () => clipboardCommand(view, "cut")
114
123
  },
115
- {
116
- label: "Copy",
117
- icon: Copy,
118
- hint: `${MOD}C`,
119
- enabled: selected,
120
- run: () => clipboardCommand(view, "copy")
121
- },
124
+ copy,
122
125
  {
123
126
  label: "Paste",
124
127
  icon: ClipboardPaste,
@@ -134,7 +137,7 @@ function TextMenu({
134
137
  enabled: selected && !shut,
135
138
  run: () => run(deleteSelection)
136
139
  }
137
- ]
140
+ ] : [copy]
138
141
  ];
139
142
  groups.push([
140
143
  {
@@ -144,7 +147,9 @@ function TextMenu({
144
147
  run: () => onAddComment?.()
145
148
  }
146
149
  ]);
147
- if (allowLocking) groups.push([lockItem(selectionLock(state), run)]);
150
+ if (allowLocking && bodyOpen) {
151
+ groups.push([lockItem(selectionLock(state), run)]);
152
+ }
148
153
  const choose = (item) => {
149
154
  if (!item.enabled) return;
150
155
  item.run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portone/docx-editor",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "A simple DOCX editor for React, built directly on OOXML.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -76,10 +76,12 @@
76
76
  "@biomejs/biome": "2.5.10",
77
77
  "@changesets/changelog-github": "^1.0.0",
78
78
  "@changesets/cli": "^3.0.1",
79
+ "@microsoft/api-extractor": "7.59.0",
79
80
  "@playwright/test": "^1.62.1",
81
+ "@types/jsdom": "^30.0.0",
80
82
  "@types/node": "^26.2.0",
81
83
  "@types/react": "19.2.18",
82
- "@types/react-dom": "19.2.4",
84
+ "@types/react-dom": "19.2.5",
83
85
  "@vitejs/plugin-react": "^6.0.5",
84
86
  "esbuild": "^0.28.1",
85
87
  "jsdom": "^30.0.1",
@@ -111,6 +113,7 @@
111
113
  "test": "vitest run",
112
114
  "test:package": "vitest run --dir packaging --no-file-parallelism",
113
115
  "verify:package": "node scripts/verify-package.mjs",
116
+ "api:update": "pnpm build && node scripts/api-report.mjs --local",
114
117
  "spec": "node scripts/index-ooxml-spec.mjs",
115
118
  "changeset": "changeset",
116
119
  "changeset:version": "changeset version",
@@ -1,3 +0,0 @@
1
- /** Prevents ordinary editor transactions from dropping imported bookmark range markers. */
2
- import { Plugin } from "prosemirror-state";
3
- export declare function bookmarkProtection(): Plugin;
@@ -1,28 +0,0 @@
1
- // src/editor/plugins/bookmarkProtection.ts
2
- import { Plugin } from "prosemirror-state";
3
- var BOOKMARK_XML = /<(?:[\w.-]+:)?bookmark(?:Start|End)\b/;
4
- function signatures(doc) {
5
- const found = [];
6
- doc.descendants((node) => {
7
- if (node.type.name === "bookmarkBlock") {
8
- found.push(`block:${node.attrs.srcId}:${node.attrs.name}`);
9
- } else if (node.type.name === "rawInline" && typeof node.attrs.xml === "string" && BOOKMARK_XML.test(node.attrs.xml)) {
10
- found.push(`inline:${node.attrs.xml}`);
11
- }
12
- return true;
13
- });
14
- return found;
15
- }
16
- function same(a, b) {
17
- return a.length === b.length && a.every((value, index) => value === b[index]);
18
- }
19
- function bookmarkProtection() {
20
- return new Plugin({
21
- filterTransaction(transaction, state) {
22
- return !transaction.docChanged || same(signatures(state.doc), signatures(transaction.doc));
23
- }
24
- });
25
- }
26
- export {
27
- bookmarkProtection
28
- };
@@ -1,3 +0,0 @@
1
- /** Keeps imported footnote and endnote references intact while their bodies are display-only. */
2
- import { Plugin } from "prosemirror-state";
3
- export declare function noteProtection(): Plugin;
@@ -1,25 +0,0 @@
1
- // src/editor/plugins/noteProtection.ts
2
- import { Plugin } from "prosemirror-state";
3
- function signatures(doc) {
4
- const found = [];
5
- doc.descendants((node) => {
6
- if (node.type.name === "noteReference") {
7
- found.push(JSON.stringify(node.attrs));
8
- }
9
- return true;
10
- });
11
- return found;
12
- }
13
- function same(a, b) {
14
- return a.length === b.length && a.every((value, index) => value === b[index]);
15
- }
16
- function noteProtection() {
17
- return new Plugin({
18
- filterTransaction(transaction, state) {
19
- return !transaction.docChanged || same(signatures(state.doc), signatures(transaction.doc));
20
- }
21
- });
22
- }
23
- export {
24
- noteProtection
25
- };