@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
@@ -9,6 +9,7 @@ import {
9
9
  insertionInsideLocked,
10
10
  rangeTouchesLocked
11
11
  } from "../../../schema/locks.js";
12
+ import { editsShut } from "../../../schema/protectionState.js";
12
13
  import {
13
14
  activePieces,
14
15
  caretPiece,
@@ -69,9 +70,13 @@ function applyToCaret(state, dispatch, edit) {
69
70
  return true;
70
71
  }
71
72
  function runEditCommand(edit) {
72
- return (state, dispatch) => state.selection.empty ? applyToCaret(state, dispatch, edit) : applyToSelection(state, dispatch, edit);
73
+ return (state, dispatch) => {
74
+ if (editsShut(state)) return false;
75
+ return state.selection.empty ? applyToCaret(state, dispatch, edit) : applyToSelection(state, dispatch, edit);
76
+ };
73
77
  }
74
78
  function canFormatText(state) {
79
+ if (editsShut(state)) return false;
75
80
  if (state.selection.empty) {
76
81
  return !insertionInsideLocked(state.doc, state.selection.from);
77
82
  }
@@ -1,12 +1,21 @@
1
1
  // src/editor/commands/historyCommands.ts
2
2
  import { redo as historyRedo, undo as historyUndo } from "prosemirror-history";
3
+ import { transactionAllowed } from "../../schema/guards.js";
3
4
  import { historyReplay } from "../../schema/locks.js";
4
5
  function replaying(command) {
5
- return (state, dispatch, view) => command(
6
- state,
7
- dispatch && ((tr) => dispatch(tr.setMeta(historyReplay, true))),
8
- view
9
- );
6
+ return (state, dispatch, view) => {
7
+ const built = [];
8
+ const answered = command(
9
+ state,
10
+ (tr) => built.push(tr.setMeta(historyReplay, true)),
11
+ view
12
+ );
13
+ if (!answered || !built.every((tr) => transactionAllowed(tr, state))) {
14
+ return false;
15
+ }
16
+ if (dispatch) for (const tr of built) dispatch(tr);
17
+ return true;
18
+ };
10
19
  }
11
20
  var undo = replaying(historyUndo);
12
21
  var redo = replaying(historyRedo);
@@ -15,6 +15,12 @@ export type { LineSpacing, ParagraphAlign } from "../../model/format";
15
15
  export type { ListKind } from "../../numbering/listTemplate";
16
16
  /** The size an image is shown at, which the insert command asks for */
17
17
  export type { ImageExtent } from "../../ooxml/image";
18
+ /**
19
+ * What the editor as a whole may receive - everything, comments alone, or nothing - which a control
20
+ * of your own reads before it offers an edit, the way the built-in ones do.
21
+ */
22
+ export type { EditableComments, EditingProtection, } from "../../schema/protection";
23
+ export { editingProtection } from "../../schema/protectionState";
18
24
  /** The fonts to draw with where a document names one the machine does not have, which the font queries read */
19
25
  export type { FontFallbacks } from "../../styles/fontStack";
20
26
  /**
@@ -49,7 +55,7 @@ export { insertLineBreak, insertPageBreak } from "./breakCommands";
49
55
  */
50
56
  export { canRunCommand } from "./canRunCommand";
51
57
  export type { CommentAuthor, DocumentComment, DocumentCommentReply, NewComment, } from "./commentCommands";
52
- export { addComment, addCommentReply, canAddComment, documentComments, removeComment, removeCommentReply, selectComment, setCommentResolved, updateComment, updateCommentReply, } from "./commentCommands";
58
+ export { addComment, addCommentReply, canAddComment, canEditComment, documentComments, removeComment, removeCommentReply, selectComment, setCommentResolved, updateComment, updateCommentReply, } from "./commentCommands";
53
59
  export type { ActiveFontFamily, ActiveFontSize, } from "./formattingCommands";
54
60
  export { activeFontFamily, activeFontSize, activeTextBackground, activeTextColor, canFormatText, documentFontNames, isBoldActive, isItalicActive, isStrikeActive, isUnderlineActive, setFontFamily, setFontSize, setTextBackground, setTextColor, toggleBold, toggleItalic, toggleStrike, toggleUnderline, } from "./formattingCommands";
55
61
  /**
@@ -1,4 +1,5 @@
1
1
  // src/editor/commands/index.ts
2
+ import { editingProtection } from "../../schema/protectionState.js";
2
3
  import {
3
4
  documentBodyWidthPx,
4
5
  documentDefaults,
@@ -19,6 +20,7 @@ import {
19
20
  addComment,
20
21
  addCommentReply,
21
22
  canAddComment,
23
+ canEditComment,
22
24
  documentComments,
23
25
  removeComment,
24
26
  removeCommentReply,
@@ -108,6 +110,7 @@ export {
108
110
  addCommentReply,
109
111
  canAddComment,
110
112
  canDecreaseIndent,
113
+ canEditComment,
111
114
  canFormatText,
112
115
  canIncreaseIndent,
113
116
  canInsertImage,
@@ -125,6 +128,7 @@ export {
125
128
  documentHasLocked,
126
129
  documentNotes,
127
130
  documentParagraphStyles,
131
+ editingProtection,
128
132
  fittedExtent,
129
133
  imageFilesIn,
130
134
  increaseIndent,
@@ -1,6 +1,7 @@
1
1
  // src/editor/commands/linkCommands.ts
2
2
  import { docxSchema } from "../../schema/index.js";
3
3
  import { rangeTouchesLocked } from "../../schema/locks.js";
4
+ import { editsShut } from "../../schema/protectionState.js";
4
5
  var linkType = docxSchema.marks.link;
5
6
  function text(value) {
6
7
  return typeof value === "string" ? value : null;
@@ -74,6 +75,7 @@ function linkSpansTouching(doc, from, to) {
74
75
  return spans;
75
76
  }
76
77
  function openPieces(state) {
78
+ if (editsShut(state)) return [];
77
79
  const found = state.selection.empty ? caretPieces(state) : selectionPieces(state);
78
80
  return found.filter(
79
81
  (piece) => !rangeTouchesLocked(state.doc, piece.from, piece.to)
@@ -4,6 +4,8 @@ import {
4
4
  } from "prosemirror-state";
5
5
  import { namesNothing, newControlId } from "../../docx/sdt.js";
6
6
  import { withContentLock } from "../../docx/sdtProps.js";
7
+ import { elementXml, openTagXml } from "../../ooxml/element.js";
8
+ import { wName } from "../../ooxml/names.js";
7
9
  import { docxSchema } from "../../schema/index.js";
8
10
  import {
9
11
  carriesLock,
@@ -13,9 +15,17 @@ import {
13
15
  selectionShut,
14
16
  unlockAllowed
15
17
  } from "../../schema/locks.js";
18
+ import { editsShut } from "../../schema/protectionState.js";
16
19
  function lockedControlMark(id) {
17
20
  return docxSchema.marks.sdt.create({
18
- sdtPrefix: `<w:sdt><w:sdtPr><w:id w:val="${id}"/><w:lock w:val="sdtContentLocked"/></w:sdtPr>`,
21
+ sdtPrefix: openTagXml(wName("sdt"), null) + elementXml(
22
+ wName("sdtPr"),
23
+ [],
24
+ [
25
+ elementXml(wName("id"), [[wName("val"), `${id}`]]),
26
+ elementXml(wName("lock"), [[wName("val"), "sdtContentLocked"]])
27
+ ]
28
+ ),
19
29
  contentsLocked: true,
20
30
  deletionLocked: true
21
31
  });
@@ -88,6 +98,7 @@ function blockLockEdits(doc, block, spans, reach) {
88
98
  return [...shut, ...fresh];
89
99
  }
90
100
  function selectionLockDetail(state) {
101
+ if (editsShut(state)) return { kind: "none" };
91
102
  const selection = state.selection;
92
103
  const locking = !selection.empty && selection instanceof TextSelection;
93
104
  const edits = [];
@@ -4,7 +4,7 @@ import {
4
4
  } from "prosemirror-state";
5
5
  import { toParagraphFormat } from "../../model/format.js";
6
6
  import { docxSchema } from "../../schema/index.js";
7
- import { replacementShut } from "../../schema/locks.js";
7
+ import { editShut, selectionIntents } from "../../schema/guards.js";
8
8
  function isOrdinaryParagraph(state) {
9
9
  const { $from } = state.selection;
10
10
  if ($from.parent.type !== docxSchema.nodes.paragraph || toParagraphFormat($from.parent.attrs.format)?.numbering) {
@@ -17,7 +17,9 @@ function isOrdinaryParagraph(state) {
17
17
  }
18
18
  var insertTab = (state, dispatch) => {
19
19
  const { selection } = state;
20
- if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || replacementShut(selection, state.doc)) {
20
+ if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || selectionIntents(selection, "replace").some(
21
+ (intent) => editShut(state, intent)
22
+ )) {
21
23
  return false;
22
24
  }
23
25
  if (dispatch) {
@@ -8,7 +8,9 @@ import { type ParagraphFormatLayer, type ParagraphStyleOption, type StyleTable }
8
8
  import { type PageGeometry } from "../docx/pageGeometry";
9
9
  import type { DocumentDefaults } from "../model/format";
10
10
  import { type Numbering } from "../numbering/parseNumbering";
11
+ import type { EditableComments, EditingProtection } from "../schema/protection";
11
12
  import { type FontFallbacks } from "../styles/fontStack";
13
+ import type { CommentAuthor } from "./commands/comments/model";
12
14
  export interface EditorStateOptions {
13
15
  numbering?: Numbering;
14
16
  styles?: StyleTable;
@@ -38,22 +40,30 @@ export interface EditorStateOptions {
38
40
  reservedCommentIds?: Iterable<string>;
39
41
  /** Every paragraph id present in the opened comment parts, including orphan extension entries. */
40
42
  reservedCommentParaIds?: Iterable<string>;
43
+ /**
44
+ * What the document as a whole may receive (`schema/protection`): everything, comments alone,
45
+ * or nothing. Everything when none is given, which is what every state was before.
46
+ */
47
+ protection?: EditingProtection;
48
+ /** Whose comments are written, and so whose may be edited under `editableComments: "own"` */
49
+ author?: CommentAuthor | null;
50
+ /** Whose comments may be edited or deleted. One's own when none is given */
51
+ editableComments?: EditableComments;
41
52
  }
42
53
  /**
43
54
  * Creates a single editing state.
44
55
  * The list definitions, the style table, and the document defaults are what the document
45
56
  * wrote down, and commands and the toolbar read them through plugins.
46
57
  */
47
- export declare function createEditorState(doc: PMNode, { numbering, styles, defaults, paragraphDefaults, canStartNewList, consumerPlugins, paragraphStyles, contextMenus, geometry, defaultTabStopPt, reservedCommentIds, reservedCommentParaIds, }?: EditorStateOptions): EditorState;
58
+ export declare function createEditorState(doc: PMNode, { numbering, styles, defaults, paragraphDefaults, canStartNewList, consumerPlugins, paragraphStyles, contextMenus, geometry, defaultTabStopPt, reservedCommentIds, reservedCommentParaIds, protection, author, editableComments, }?: EditorStateOptions): EditorState;
48
59
  export interface EditorOptions {
49
60
  mount: HTMLElement;
50
61
  state: EditorState;
51
62
  defaults: DocumentDefaults;
52
63
  /** The paper the document names. The sheet is drawn from it, A4 where a document names none */
53
64
  geometry?: PageGeometry;
54
- readOnly: boolean;
55
65
  /** The fonts stood in for the ones the document declares. The built-in set when none is given */
56
66
  fontFallbacks?: FontFallbacks;
57
67
  onStateChange: (state: EditorState) => void;
58
68
  }
59
- export declare function createEditorView({ mount, state, defaults, geometry, readOnly, fontFallbacks, onStateChange, }: EditorOptions): EditorView;
69
+ export declare function createEditorView({ mount, state, defaults, geometry, fontFallbacks, onStateChange, }: EditorOptions): EditorView;
@@ -15,6 +15,7 @@ import { A4_PORTRAIT } from "../docx/pageGeometry.js";
15
15
  import { EMPTY_NUMBERING } from "../numbering/parseNumbering.js";
16
16
  import { pageDecorations } from "../page/pageDecorations.js";
17
17
  import { pageGeometryStyle, pagePixels } from "../page/pageLayout.js";
18
+ import { editsShut } from "../schema/protectionState.js";
18
19
  import { editorClassNames } from "../styles/classNames.js";
19
20
  import {
20
21
  DEFAULT_FONT_FALLBACKS
@@ -24,16 +25,15 @@ import { gridBorders, withDerivedGridBorders } from "../table/gridBorders.js";
24
25
  import { documentDefaultTabStopPt, documentStyleTable } from "./documentStyles.js";
25
26
  import { externalClipboard } from "./externalClipboard.js";
26
27
  import { imageFiles } from "./imageFiles.js";
27
- import { bookmarkProtection } from "./plugins/bookmarkProtection.js";
28
28
  import { columnResize } from "./plugins/columnResize.js";
29
29
  import { commentDecorations } from "./plugins/commentDecorations.js";
30
30
  import { commentReservations } from "./plugins/commentReservations.js";
31
+ import { documentProtection } from "./plugins/documentProtection.js";
31
32
  import { imagePaste } from "./plugins/imagePaste.js";
32
- import { docxKeymap } from "./plugins/keymap.js";
33
+ import { docxKeymap, historyKeys } from "./plugins/keymap.js";
33
34
  import { linkPanel } from "./plugins/linkPanel.js";
34
35
  import { listInputRules } from "./plugins/listInputRules.js";
35
36
  import { lockedContent } from "./plugins/lockedContent.js";
36
- import { noteProtection } from "./plugins/noteProtection.js";
37
37
  import { numberingMarkers } from "./plugins/numberingDecorations.js";
38
38
  import { rowResize } from "./plugins/rowResize.js";
39
39
  import { styledParagraphs } from "./plugins/styledParagraphs.js";
@@ -57,7 +57,10 @@ function createEditorState(doc, {
57
57
  geometry = A4_PORTRAIT,
58
58
  defaultTabStopPt = DEFAULT_TAB_STOP_PT,
59
59
  reservedCommentIds = [],
60
- reservedCommentParaIds = []
60
+ reservedCommentParaIds = [],
61
+ protection = "none",
62
+ author = null,
63
+ editableComments = "own"
61
64
  } = {}) {
62
65
  return EditorState.create({
63
66
  doc: withDerivedGridBorders(doc),
@@ -66,17 +69,16 @@ function createEditorState(doc, {
66
69
  // first answer for a keypress, a paste, a drop or any other DOM event, so this is the
67
70
  // only place from which a consumer handler can win over the built-in one.
68
71
  ...consumerPlugins,
69
- // Refuses every edit inside a locked content control, whoever asked for it. It is not
70
- // optional: a document that locked a part of itself stays locked in every consumer.
72
+ // Refuses every edit no guard in `schema/guards` lets through, whoever asked for it. It is
73
+ // not optional: a document that locked a part of itself stays locked in every consumer, and
74
+ // the preserved bookmark markers and note references stay where the file put them.
71
75
  lockedContent(),
72
- // Bookmark ranges are preserved rather than edited, so neither half may disappear.
73
- bookmarkProtection(),
74
- // Note bodies are display-only, so their imported main-story references stay paired.
75
- noteProtection(),
76
+ documentProtection({ protection, author, editableComments }),
76
77
  // An orphan Comments-part entry still owns its id and must not be replaced by a new comment.
77
78
  commentReservations(reservedCommentIds, reservedCommentParaIds),
78
79
  history(),
79
80
  keymap(docxKeymap),
81
+ historyKeys(),
80
82
  keymap(baseKeymap),
81
83
  // Holds whether the link panel is open, which Cmd+K above and the toolbar button both set
82
84
  linkPanel(),
@@ -128,18 +130,24 @@ function createEditorView({
128
130
  state,
129
131
  defaults,
130
132
  geometry = A4_PORTRAIT,
131
- readOnly,
132
133
  fontFallbacks = DEFAULT_FONT_FALLBACKS,
133
134
  onStateChange
134
135
  }) {
136
+ const sheetStyle = `${pageGeometryStyle(pagePixels(geometry))};${documentDefaultsStyle(defaults, fontFallbacks)};`;
135
137
  const view = new EditorView(mount, {
136
138
  state,
137
- editable: () => !readOnly,
138
- attributes: {
139
+ // A protection that shuts the body shuts typing with it, and is read off the state so that a
140
+ // mode switched on an open document takes effect without a new view. Selecting stays open
141
+ // either way, which is what a reader marking a stretch for a comment needs
142
+ editable: (current) => !editsShut(current),
143
+ attributes: (current) => ({
139
144
  class: editorClassNames.sheet,
140
145
  // The paper first, so a document that names one is drawn on it from the first frame
141
- style: `${pageGeometryStyle(pagePixels(geometry))};${documentDefaultsStyle(defaults, fontFallbacks)};tab-size:${documentDefaultTabStopPt(state)}pt`
142
- },
146
+ style: `${sheetStyle}tab-size:${documentDefaultTabStopPt(current)}pt`,
147
+ // A sheet that takes no typing is no longer focusable of itself, so a reader or a commenter
148
+ // is handed the focus another way: the keys reach it, and the selection stays its own
149
+ ...editsShut(current) ? { tabindex: "0" } : {}
150
+ }),
143
151
  // The schema can only draw a run with the default fallback fonts, so this editor draws its own
144
152
  markViews: { run: runMarkView(fontFallbacks) },
145
153
  // An image is drawn by a view of its own, which is what carries the resize handles
@@ -1,16 +1,23 @@
1
1
  // src/editor/externalClipboard.ts
2
- import { Fragment, Slice } from "prosemirror-model";
2
+ import {
3
+ DOMSerializer,
4
+ Fragment,
5
+ Slice
6
+ } from "prosemirror-model";
3
7
  import { Plugin } from "prosemirror-state";
4
8
  import { effectiveParagraphFormat, styleIdOf } from "../docx/formatting.js";
5
9
  import { withParagraphStyle } from "../docx/paraProps.js";
6
10
  import { MAX_ILVL, nextNumId } from "../numbering/listTemplate.js";
7
- import { docxSchema } from "../schema/index.js";
11
+ import { elementXml } from "../ooxml/element.js";
12
+ import { wName } from "../ooxml/names.js";
13
+ import { docxSchema, isPageBreak } from "../schema/index.js";
8
14
  import { editorClassNames } from "../styles/classNames.js";
9
15
  import { PASTED_IMAGE_ATTRIBUTE } from "./clipboard/images.js";
10
16
  import {
11
17
  appendInline,
12
18
  contextFor,
13
19
  marksFor,
20
+ safeHref,
14
21
  withInlineStyle
15
22
  } from "./clipboard/inlineFormatting.js";
16
23
  import { listRefOf } from "./commands/listCommands.js";
@@ -59,11 +66,149 @@ var HEADING_FALLBACKS = {
59
66
  5: { bold: true, fontSizePt: 10 },
60
67
  6: { bold: true, fontSizePt: 8 }
61
68
  };
69
+ var PUBLIC_ATTRIBUTES = /* @__PURE__ */ new Set([
70
+ "alt",
71
+ "aria-label",
72
+ "class",
73
+ "colspan",
74
+ "height",
75
+ "href",
76
+ "lang",
77
+ "rowspan",
78
+ "src",
79
+ "style",
80
+ "width"
81
+ ]);
82
+ var COPIED_STYLE_ATTRIBUTE = "data-style";
83
+ function isAttributes(value) {
84
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Node);
85
+ }
86
+ function publicAttributes(attrs) {
87
+ return Object.fromEntries(
88
+ Object.entries(attrs).filter(([name]) => PUBLIC_ATTRIBUTES.has(name))
89
+ );
90
+ }
91
+ function isSpecArray(value) {
92
+ return Array.isArray(value) && typeof value[0] === "string";
93
+ }
94
+ function stripPrivateAttributes(spec) {
95
+ if (!isSpecArray(spec)) return spec;
96
+ const [tag, ...rest] = spec;
97
+ const attrs = isAttributes(rest[0]) ? publicAttributes(rest[0]) : null;
98
+ const children = (attrs === null ? rest : rest.slice(1)).map(
99
+ (child) => isSpecArray(child) ? stripPrivateAttributes(child) : child
100
+ );
101
+ return attrs === null ? [tag, ...children] : [tag, attrs, ...children];
102
+ }
103
+ function withDomAttribute(spec, name, value) {
104
+ if (!isSpecArray(spec)) return spec;
105
+ const [tag, ...rest] = spec;
106
+ return isAttributes(rest[0]) ? [tag, { ...rest[0], [name]: value }, ...rest.slice(1)] : [tag, { [name]: value }, ...rest];
107
+ }
108
+ function asTag(spec, tag) {
109
+ return isSpecArray(spec) ? [tag, ...spec.slice(1)] : spec;
110
+ }
111
+ function copiedMarkSpec(mark, spec) {
112
+ const stripped = stripPrivateAttributes(spec);
113
+ if (mark.type !== docxSchema.marks.link) return stripped;
114
+ const href = safeHref(
115
+ typeof mark.attrs.href === "string" ? mark.attrs.href : null
116
+ );
117
+ return href === null ? stripped : asTag(withDomAttribute(stripped, "href", href), "a");
118
+ }
119
+ function copiedNodeSpec(node, spec) {
120
+ const stripped = stripPrivateAttributes(spec);
121
+ if (node.type !== docxSchema.nodes.paragraph) return stripped;
122
+ const styleId = styleIdOf(node.attrs.pPr);
123
+ return styleId === null ? stripped : withDomAttribute(stripped, COPIED_STYLE_ATTRIBUTE, styleId);
124
+ }
125
+ function clipboardSerializer() {
126
+ const drawn = DOMSerializer.fromSchema(docxSchema);
127
+ const nodes = Object.fromEntries(
128
+ Object.entries(drawn.nodes).map(([name, toDOM]) => [
129
+ name,
130
+ (node) => copiedNodeSpec(node, toDOM(node))
131
+ ])
132
+ );
133
+ const marks = Object.fromEntries(
134
+ Object.entries(drawn.marks).map(([name, toDOM]) => [
135
+ name,
136
+ (mark, inline) => copiedMarkSpec(mark, toDOM(mark, inline))
137
+ ])
138
+ );
139
+ return new DOMSerializer(nodes, marks);
140
+ }
141
+ var UNREADABLE_BLOCKS = /* @__PURE__ */ new Set([
142
+ "bookmarkBlock",
143
+ "docxRaw",
144
+ "rawBlock"
145
+ ]);
146
+ function stringAttr(value) {
147
+ return typeof value === "string" ? value : "";
148
+ }
149
+ function inlineText(node) {
150
+ if (node.isText) return node.text ?? "";
151
+ if (node.type === docxSchema.nodes.hardBreak) {
152
+ return isPageBreak(node.attrs.brAttrs) ? "\f" : "\n";
153
+ }
154
+ if (node.type === docxSchema.nodes.image) return stringAttr(node.attrs.alt);
155
+ if (node.type === docxSchema.nodes.noteReference) {
156
+ return node.attrs.customMarkFollows === true ? "" : stringAttr(node.attrs.label);
157
+ }
158
+ return "";
159
+ }
160
+ function rowsText(table) {
161
+ const rows = [];
162
+ table.forEach((row) => {
163
+ const cells = [];
164
+ row.forEach((cell) => {
165
+ cells.push(fragmentText(cell.content));
166
+ });
167
+ rows.push(cells.join(" "));
168
+ });
169
+ return rows.join("\n");
170
+ }
171
+ function blockText(node) {
172
+ if (node.type === docxSchema.nodes.table) return rowsText(node);
173
+ if (UNREADABLE_BLOCKS.has(node.type.name)) return "";
174
+ return fragmentText(node.content);
175
+ }
176
+ function fragmentText(fragment) {
177
+ const pieces = [];
178
+ fragment.forEach((child) => {
179
+ pieces.push(child.isInline ? inlineText(child) : blockText(child));
180
+ });
181
+ return pieces.join(fragment.firstChild?.isInline === true ? "" : "\n");
182
+ }
183
+ function bareWrappers(content, openStart, openEnd) {
184
+ const wrapper = content.firstChild;
185
+ if (openStart <= 1 || openEnd <= 1 || content.childCount !== 1)
186
+ return content;
187
+ if (wrapper === null || wrapper.childCount !== 1) return content;
188
+ return Fragment.from(
189
+ wrapper.type.create(
190
+ null,
191
+ bareWrappers(wrapper.content, openStart - 1, openEnd - 1),
192
+ wrapper.marks
193
+ )
194
+ );
195
+ }
196
+ function copiedSlice(slice) {
197
+ return new Slice(
198
+ bareWrappers(slice.content, slice.openStart, slice.openEnd),
199
+ slice.openStart,
200
+ slice.openEnd
201
+ );
202
+ }
203
+ function clipboardText(slice) {
204
+ return fragmentText(slice.content);
205
+ }
62
206
  function normalizedStyleName(value) {
63
207
  return value.toLowerCase().replace(/[\s_-]+/g, "");
64
208
  }
65
209
  function copiedStyleId(element) {
66
- return element.classList.contains(editorClassNames.paragraph) ? styleIdOf(element.getAttribute("data-ppr")) : null;
210
+ if (!element.classList.contains(editorClassNames.paragraph)) return null;
211
+ return element.getAttribute(COPIED_STYLE_ATTRIBUTE) ?? styleIdOf(element.getAttribute("data-ppr"));
67
212
  }
68
213
  function headingLevel(element, sourceStyleId) {
69
214
  const direct = HEADING_LEVELS[element.tagName];
@@ -116,7 +261,20 @@ function blockContext(state, parent, element, preserveParagraphStyle) {
116
261
  function listParagraphAttrs(state, list) {
117
262
  if (!list || list.numId === null) return null;
118
263
  const ilvl = Math.min(MAX_ILVL, list.level);
119
- const pPr = `<w:pPr><w:numPr><w:ilvl w:val="${ilvl}"/><w:numId w:val="${list.numId}"/></w:numPr></w:pPr>`;
264
+ const pPr = elementXml(
265
+ wName("pPr"),
266
+ [],
267
+ [
268
+ elementXml(
269
+ wName("numPr"),
270
+ [],
271
+ [
272
+ elementXml(wName("ilvl"), [[wName("val"), `${ilvl}`]]),
273
+ elementXml(wName("numId"), [[wName("val"), `${list.numId}`]])
274
+ ]
275
+ )
276
+ ]
277
+ );
120
278
  return {
121
279
  pPr,
122
280
  format: effectiveParagraphFormat(pPr, documentParagraphFormatting(state))
@@ -294,8 +452,12 @@ function insertClipboardData(view, data) {
294
452
  insertPlainText(view, data.text ?? "");
295
453
  }
296
454
  function externalClipboard() {
455
+ const serializer = clipboardSerializer();
297
456
  return new Plugin({
298
457
  props: {
458
+ clipboardSerializer: serializer,
459
+ clipboardTextSerializer: clipboardText,
460
+ transformCopied: copiedSlice,
299
461
  handlePaste(view, event) {
300
462
  insertClipboardData(view, {
301
463
  html: event.clipboardData?.getData("text/html"),
@@ -2,9 +2,13 @@
2
2
  import { insertPoint } from "prosemirror-transform";
3
3
  import { toImageExtent, toImageSrc } from "../ooxml/image.js";
4
4
  import { docxSchema } from "../schema/index.js";
5
- import { replacementShut } from "../schema/locks.js";
5
+ import { editShut, selectionIntents } from "../schema/guards.js";
6
6
  function insertPosition(state) {
7
- if (replacementShut(state.selection, state.doc)) return null;
7
+ if (selectionIntents(state.selection, "replace").some(
8
+ (intent) => editShut(state, intent)
9
+ )) {
10
+ return null;
11
+ }
8
12
  return insertPoint(state.doc, state.selection.from, docxSchema.nodes.image);
9
13
  }
10
14
  function canInsertImage(state) {
@@ -4,6 +4,7 @@ import {
4
4
  TextSelection
5
5
  } from "prosemirror-state";
6
6
  import { createTableNode, isTableSide } from "../docx/tableTemplate.js";
7
+ import { editsShut } from "../schema/protectionState.js";
7
8
  import { documentGeometry } from "./documentStyles.js";
8
9
  function isInTable($pos) {
9
10
  for (let depth = $pos.depth; depth > 0; depth -= 1) {
@@ -13,7 +14,7 @@ function isInTable($pos) {
13
14
  }
14
15
  function insertPosition(state) {
15
16
  const $from = state.selection.$from;
16
- if ($from.depth === 0 || isInTable($from)) return null;
17
+ if (editsShut(state) || $from.depth === 0 || isInTable($from)) return null;
17
18
  return $from.after(1);
18
19
  }
19
20
  function canInsertTable(state) {
@@ -28,6 +28,8 @@ export declare function selectedParagraphs(state: EditorState): ParagraphSpot[];
28
28
  * Only the paragraphs of a locked cell are shut. A paragraph merely holding a locked control keeps
29
29
  * its own alignment, indent and style, which is what `insideLockedCell` says and
30
30
  * `rangeTouchesLocked` would not (`schema/locks`).
31
+ *
32
+ * A protection that shuts the body leaves no paragraph editable (`schema/protection`).
31
33
  */
32
34
  export declare function editableParagraphs(state: EditorState): ParagraphSpot[];
33
35
  /** The original formatting XML this paragraph holds. Null when there is none */
@@ -2,6 +2,7 @@
2
2
  import { effectiveParagraphFormat } from "../docx/formatting.js";
3
3
  import { docxSchema } from "../schema/index.js";
4
4
  import { insideLockedCell } from "../schema/locks.js";
5
+ import { editsShut } from "../schema/protectionState.js";
5
6
  import {
6
7
  defaultParagraphStyleId,
7
8
  documentParagraphFormatting,
@@ -19,6 +20,7 @@ function selectedParagraphs(state) {
19
20
  return Array.from(spots, ([pos, node]) => ({ pos, node }));
20
21
  }
21
22
  function editableParagraphs(state) {
23
+ if (editsShut(state)) return [];
22
24
  return selectedParagraphs(state).filter(
23
25
  (spot) => !insideLockedCell(state.doc, spot.pos)
24
26
  );
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Holds the protection the editor runs under (`schema/protection`) in the state, where the guards
3
+ * and the commands read it. The runtime filter stands in `lockedContent`, which enforces every
4
+ * guard `schema/guards` registers, the protection among them, in one pass.
5
+ */
6
+ import { Plugin, type Transaction } from "prosemirror-state";
7
+ import type { EditableComments, EditingProtection, ProtectionState } from "../../schema/protection";
8
+ import type { CommentAuthor } from "../commands/comments/model";
9
+ export interface DocumentProtectionOptions {
10
+ protection: EditingProtection;
11
+ /** Whose comments are being written. Null where none are, a reader above all */
12
+ author: CommentAuthor | null;
13
+ editableComments: EditableComments;
14
+ }
15
+ /**
16
+ * The transaction that puts the editor under another protection, which is how a mode is switched
17
+ * on a document already open: `reconfigure` keeps the state a plugin of the same key already
18
+ * holds, so a new plugin instance would not do it.
19
+ */
20
+ export declare function setProtection(tr: Transaction, options: DocumentProtectionOptions): Transaction;
21
+ export declare function documentProtection(options: DocumentProtectionOptions): Plugin<ProtectionState>;
@@ -0,0 +1,44 @@
1
+ // src/editor/plugins/documentProtection.ts
2
+ import { Plugin } from "prosemirror-state";
3
+ import { protectionKey } from "../../schema/protectionState.js";
4
+ function rulesOf({
5
+ protection,
6
+ author,
7
+ editableComments
8
+ }) {
9
+ return { protection, authorId: author?.id ?? null, editableComments };
10
+ }
11
+ var PROTECTIONS = [
12
+ "none",
13
+ "readOnly",
14
+ "comments"
15
+ ];
16
+ var EDITABLE_COMMENTS = ["own", "all"];
17
+ function isOneOf(values, candidate) {
18
+ return typeof candidate === "string" && values.some((value) => value === candidate);
19
+ }
20
+ function isRules(value) {
21
+ if (typeof value !== "object" || value === null) return false;
22
+ const authorId = Reflect.get(value, "authorId");
23
+ return isOneOf(PROTECTIONS, Reflect.get(value, "protection")) && (authorId === null || typeof authorId === "string") && isOneOf(EDITABLE_COMMENTS, Reflect.get(value, "editableComments"));
24
+ }
25
+ function setProtection(tr, options) {
26
+ return tr.setMeta(protectionKey, rulesOf(options));
27
+ }
28
+ function documentProtection(options) {
29
+ const rules = rulesOf(options);
30
+ return new Plugin({
31
+ key: protectionKey,
32
+ state: {
33
+ init: () => rules,
34
+ apply: (transaction, value) => {
35
+ const next = transaction.getMeta(protectionKey);
36
+ return isRules(next) ? next : value;
37
+ }
38
+ }
39
+ });
40
+ }
41
+ export {
42
+ documentProtection,
43
+ setProtection
44
+ };