@portone/docx-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -5
  3. package/dist/DocxEditor.d.ts +34 -10
  4. package/dist/DocxEditor.js +69 -24
  5. package/dist/core.d.ts +2 -0
  6. package/dist/core.js +2 -0
  7. package/dist/docx/commentOnlyChange.d.ts +45 -0
  8. package/dist/docx/commentOnlyChange.js +145 -0
  9. package/dist/docx/comments/constants.d.ts +8 -0
  10. package/dist/docx/comments/constants.js +6 -0
  11. package/dist/docx/comments/contentTypes.d.ts +7 -0
  12. package/dist/docx/comments/contentTypes.js +38 -0
  13. package/dist/docx/comments/model.d.ts +2 -0
  14. package/dist/docx/comments/model.js +3 -0
  15. package/dist/docx/comments/people.d.ts +39 -0
  16. package/dist/docx/comments/people.js +198 -0
  17. package/dist/docx/comments/reading.d.ts +16 -1
  18. package/dist/docx/comments/reading.js +15 -5
  19. package/dist/docx/comments/writing.d.ts +4 -1
  20. package/dist/docx/comments/writing.js +12 -32
  21. package/dist/docx/importParagraph.js +1 -0
  22. package/dist/editor/commands/breakCommands.js +4 -1
  23. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  24. package/dist/editor/commands/canRunCommand.js +1 -1
  25. package/dist/editor/commands/comments/editing.js +6 -4
  26. package/dist/editor/commands/comments/model.d.ts +9 -0
  27. package/dist/editor/commands/comments/reading.d.ts +7 -0
  28. package/dist/editor/commands/comments/reading.js +14 -0
  29. package/dist/editor/commands/formatting/editing.js +6 -1
  30. package/dist/editor/commands/formatting/reading.js +13 -4
  31. package/dist/editor/commands/historyCommands.js +14 -6
  32. package/dist/editor/commands/index.d.ts +7 -1
  33. package/dist/editor/commands/index.js +4 -0
  34. package/dist/editor/commands/linkCommands.js +2 -0
  35. package/dist/editor/commands/lockCommands.js +2 -0
  36. package/dist/editor/commands/tabCommands.js +2 -1
  37. package/dist/editor/createEditor.d.ts +13 -3
  38. package/dist/editor/createEditor.js +21 -7
  39. package/dist/editor/insertImage.js +4 -1
  40. package/dist/editor/insertTable.js +2 -1
  41. package/dist/editor/paragraphEdits.d.ts +2 -0
  42. package/dist/editor/paragraphEdits.js +2 -0
  43. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  44. package/dist/editor/plugins/documentProtection.js +44 -0
  45. package/dist/editor/plugins/imagePaste.js +4 -1
  46. package/dist/editor/plugins/keymap.d.ts +11 -1
  47. package/dist/editor/plugins/keymap.js +24 -4
  48. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  49. package/dist/editor/plugins/lockedContent.js +1 -1
  50. package/dist/editor/plugins/tableContextMenu.js +2 -1
  51. package/dist/editor/plugins/textContextMenu.js +6 -1
  52. package/dist/index.d.ts +2 -0
  53. package/dist/numbering/markers.js +13 -1
  54. package/dist/schema/docxSchema.js +5 -0
  55. package/dist/schema/locks.d.ts +31 -3
  56. package/dist/schema/locks.js +54 -3
  57. package/dist/schema/protection.d.ts +77 -0
  58. package/dist/schema/protection.js +170 -0
  59. package/dist/schema/protectionState.d.ts +20 -0
  60. package/dist/schema/protectionState.js +37 -0
  61. package/dist/styles/fontStack.d.ts +9 -0
  62. package/dist/styles/fontStack.js +9 -8
  63. package/dist/table/cellFormatting.js +3 -1
  64. package/dist/table/commands.js +1 -1
  65. package/dist/table/merge.js +2 -2
  66. package/dist/ui/CommentsPanel.d.ts +3 -3
  67. package/dist/ui/CommentsPanel.js +26 -18
  68. package/dist/ui/FontFamilySelect.js +21 -7
  69. package/dist/ui/LinkCard.d.ts +1 -2
  70. package/dist/ui/LinkCard.js +3 -4
  71. package/dist/ui/TextMenu.js +15 -10
  72. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
1
  // src/editor/commands/comments/reading.ts
2
+ import { commentOwned } from "../../../schema/protection.js";
3
+ import { protectionOf } from "../../../schema/protectionState.js";
2
4
  function stringAttr(value) {
3
5
  return typeof value === "string" ? value : null;
4
6
  }
@@ -34,6 +36,7 @@ function documentComments(state) {
34
36
  return {
35
37
  id,
36
38
  author: stringAttr(node.attrs.author),
39
+ authorId: stringAttr(node.attrs.authorId),
37
40
  initials: stringAttr(node.attrs.initials),
38
41
  date: stringAttr(node.attrs.date),
39
42
  text: stringAttr(node.attrs.text) ?? "",
@@ -44,6 +47,7 @@ function documentComments(state) {
44
47
  replies: repliesAttr(node.attrs.replies).map((reply) => ({
45
48
  id: reply.id,
46
49
  author: reply.author,
50
+ authorId: reply.authorId,
47
51
  initials: reply.initials,
48
52
  date: reply.date,
49
53
  text: reply.text
@@ -51,7 +55,17 @@ function documentComments(state) {
51
55
  };
52
56
  });
53
57
  }
58
+ function canEditComment(state, commentId, replyId = null) {
59
+ const rules = protectionOf(state);
60
+ if (rules.protection === "readOnly") return false;
61
+ const comment = documentComments(state).find(
62
+ (entry) => entry.id === commentId
63
+ );
64
+ const body = replyId === null ? comment : comment?.replies.find((reply) => reply.id === replyId);
65
+ return body !== void 0 && commentOwned(rules, body.authorId);
66
+ }
54
67
  export {
68
+ canEditComment,
55
69
  documentComments,
56
70
  repliesAttr,
57
71
  stringAttr
@@ -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
  }
@@ -5,6 +5,7 @@ import {
5
5
  toRunFormat
6
6
  } from "../../../model/format.js";
7
7
  import {
8
+ comparableFontName,
8
9
  DEFAULT_FONT_FALLBACKS
9
10
  } from "../../../styles/fontStack.js";
10
11
  import {
@@ -40,22 +41,30 @@ function defaultFontName(state, fontFallbacks) {
40
41
  function activeFontFamily(state, fontFallbacks = DEFAULT_FONT_FALLBACKS) {
41
42
  const names = activePieces(state).map((target) => fontNameOf(target.format));
42
43
  const first = names[0] ?? null;
43
- if (names.some((name) => name !== first)) return { kind: "mixed" };
44
+ const keys = names.map(
45
+ (name) => name === null ? null : comparableFontName(name)
46
+ );
47
+ if (keys.some((key) => key !== keys[0])) return { kind: "mixed" };
44
48
  if (first !== null) return { kind: "font", name: first };
45
49
  return { kind: "default", name: defaultFontName(state, fontFallbacks) };
46
50
  }
47
51
  function documentFontNames(doc, defaults) {
48
- const names = new Set(fontNamesOf(defaults.fontFamily));
52
+ const names = /* @__PURE__ */ new Map();
53
+ const collect = (name) => {
54
+ const comparable = comparableFontName(name);
55
+ if (!names.has(comparable)) names.set(comparable, name);
56
+ };
57
+ for (const name of fontNamesOf(defaults.fontFamily)) collect(name);
49
58
  doc.descendants((node) => {
50
59
  const mark = runMarkOf(node.marks);
51
60
  for (const name of fontNamesOf(
52
61
  toRunFormat(mark?.attrs.format)?.fontFamily
53
62
  )) {
54
- names.add(name);
63
+ collect(name);
55
64
  }
56
65
  return true;
57
66
  });
58
- return Array.from(names).sort((a, b) => a.localeCompare(b));
67
+ return Array.from(names.values()).sort((a, b) => a.localeCompare(b));
59
68
  }
60
69
  function activeTextColor(state) {
61
70
  return sharedValue(
@@ -1,12 +1,20 @@
1
1
  // src/editor/commands/historyCommands.ts
2
2
  import { redo as historyRedo, undo as historyUndo } from "prosemirror-history";
3
- import { historyReplay } from "../../schema/locks.js";
3
+ import { historyReplay, transactionAllowed } from "../../schema/locks.js";
4
4
  function replaying(command) {
5
- return (state, dispatch, view) => command(
6
- state,
7
- dispatch && ((tr) => dispatch(tr.setMeta(historyReplay, true))),
8
- view
9
- );
5
+ return (state, dispatch, view) => {
6
+ const built = [];
7
+ const answered = command(
8
+ state,
9
+ (tr) => built.push(tr.setMeta(historyReplay, true)),
10
+ view
11
+ );
12
+ if (!answered || !built.every((tr) => transactionAllowed(tr, state))) {
13
+ return false;
14
+ }
15
+ if (dispatch) for (const tr of built) dispatch(tr);
16
+ return true;
17
+ };
10
18
  }
11
19
  var undo = replaying(historyUndo);
12
20
  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)
@@ -13,6 +13,7 @@ import {
13
13
  selectionShut,
14
14
  unlockAllowed
15
15
  } from "../../schema/locks.js";
16
+ import { editsShut } from "../../schema/protectionState.js";
16
17
  function lockedControlMark(id) {
17
18
  return docxSchema.marks.sdt.create({
18
19
  sdtPrefix: `<w:sdt><w:sdtPr><w:id w:val="${id}"/><w:lock w:val="sdtContentLocked"/></w:sdtPr>`,
@@ -88,6 +89,7 @@ function blockLockEdits(doc, block, spans, reach) {
88
89
  return [...shut, ...fresh];
89
90
  }
90
91
  function selectionLockDetail(state) {
92
+ if (editsShut(state)) return { kind: "none" };
91
93
  const selection = state.selection;
92
94
  const locking = !selection.empty && selection instanceof TextSelection;
93
95
  const edits = [];
@@ -5,6 +5,7 @@ import {
5
5
  import { toParagraphFormat } from "../../model/format.js";
6
6
  import { docxSchema } from "../../schema/index.js";
7
7
  import { replacementShut } from "../../schema/locks.js";
8
+ import { editsShut } from "../../schema/protectionState.js";
8
9
  function isOrdinaryParagraph(state) {
9
10
  const { $from } = state.selection;
10
11
  if ($from.parent.type !== docxSchema.nodes.paragraph || toParagraphFormat($from.parent.attrs.format)?.numbering) {
@@ -17,7 +18,7 @@ function isOrdinaryParagraph(state) {
17
18
  }
18
19
  var insertTab = (state, dispatch) => {
19
20
  const { selection } = state;
20
- if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || replacementShut(selection, state.doc)) {
21
+ if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || editsShut(state) || replacementShut(selection, state.doc)) {
21
22
  return false;
22
23
  }
23
24
  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
@@ -28,8 +29,9 @@ import { bookmarkProtection } from "./plugins/bookmarkProtection.js";
28
29
  import { columnResize } from "./plugins/columnResize.js";
29
30
  import { commentDecorations } from "./plugins/commentDecorations.js";
30
31
  import { commentReservations } from "./plugins/commentReservations.js";
32
+ import { documentProtection } from "./plugins/documentProtection.js";
31
33
  import { imagePaste } from "./plugins/imagePaste.js";
32
- import { docxKeymap } from "./plugins/keymap.js";
34
+ import { docxKeymap, historyKeys } from "./plugins/keymap.js";
33
35
  import { linkPanel } from "./plugins/linkPanel.js";
34
36
  import { listInputRules } from "./plugins/listInputRules.js";
35
37
  import { lockedContent } from "./plugins/lockedContent.js";
@@ -57,7 +59,10 @@ function createEditorState(doc, {
57
59
  geometry = A4_PORTRAIT,
58
60
  defaultTabStopPt = DEFAULT_TAB_STOP_PT,
59
61
  reservedCommentIds = [],
60
- reservedCommentParaIds = []
62
+ reservedCommentParaIds = [],
63
+ protection = "none",
64
+ author = null,
65
+ editableComments = "own"
61
66
  } = {}) {
62
67
  return EditorState.create({
63
68
  doc: withDerivedGridBorders(doc),
@@ -68,7 +73,9 @@ function createEditorState(doc, {
68
73
  ...consumerPlugins,
69
74
  // Refuses every edit inside a locked content control, whoever asked for it. It is not
70
75
  // optional: a document that locked a part of itself stays locked in every consumer.
76
+ // The same guard refuses what the protection below shuts.
71
77
  lockedContent(),
78
+ documentProtection({ protection, author, editableComments }),
72
79
  // Bookmark ranges are preserved rather than edited, so neither half may disappear.
73
80
  bookmarkProtection(),
74
81
  // Note bodies are display-only, so their imported main-story references stay paired.
@@ -77,6 +84,7 @@ function createEditorState(doc, {
77
84
  commentReservations(reservedCommentIds, reservedCommentParaIds),
78
85
  history(),
79
86
  keymap(docxKeymap),
87
+ historyKeys(),
80
88
  keymap(baseKeymap),
81
89
  // Holds whether the link panel is open, which Cmd+K above and the toolbar button both set
82
90
  linkPanel(),
@@ -128,18 +136,24 @@ function createEditorView({
128
136
  state,
129
137
  defaults,
130
138
  geometry = A4_PORTRAIT,
131
- readOnly,
132
139
  fontFallbacks = DEFAULT_FONT_FALLBACKS,
133
140
  onStateChange
134
141
  }) {
142
+ const sheetStyle = `${pageGeometryStyle(pagePixels(geometry))};${documentDefaultsStyle(defaults, fontFallbacks)};`;
135
143
  const view = new EditorView(mount, {
136
144
  state,
137
- editable: () => !readOnly,
138
- attributes: {
145
+ // A protection that shuts the body shuts typing with it, and is read off the state so that a
146
+ // mode switched on an open document takes effect without a new view. Selecting stays open
147
+ // either way, which is what a reader marking a stretch for a comment needs
148
+ editable: (current) => !editsShut(current),
149
+ attributes: (current) => ({
139
150
  class: editorClassNames.sheet,
140
151
  // 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
- },
152
+ style: `${sheetStyle}tab-size:${documentDefaultTabStopPt(current)}pt`,
153
+ // A sheet that takes no typing is no longer focusable of itself, so a reader or a commenter
154
+ // is handed the focus another way: the keys reach it, and the selection stays its own
155
+ ...editsShut(current) ? { tabindex: "0" } : {}
156
+ }),
143
157
  // The schema can only draw a run with the default fallback fonts, so this editor draws its own
144
158
  markViews: { run: runMarkView(fontFallbacks) },
145
159
  // An image is drawn by a view of its own, which is what carries the resize handles
@@ -3,8 +3,11 @@ import { insertPoint } from "prosemirror-transform";
3
3
  import { toImageExtent, toImageSrc } from "../ooxml/image.js";
4
4
  import { docxSchema } from "../schema/index.js";
5
5
  import { replacementShut } from "../schema/locks.js";
6
+ import { editsShut } from "../schema/protectionState.js";
6
7
  function insertPosition(state) {
7
- if (replacementShut(state.selection, state.doc)) return null;
8
+ if (editsShut(state) || replacementShut(state.selection, state.doc)) {
9
+ return null;
10
+ }
8
11
  return insertPoint(state.doc, state.selection.from, docxSchema.nodes.image);
9
12
  }
10
13
  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 guard
3
+ * and the commands read it. The guard itself stands in `lockedContent`, which enforces
4
+ * `transactionAllowed` for locks and protection 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
+ };
@@ -3,6 +3,7 @@ import { isHistoryTransaction } from "prosemirror-history";
3
3
  import { Plugin, PluginKey } from "prosemirror-state";
4
4
  import { Decoration, DecorationSet } from "prosemirror-view";
5
5
  import { replacementShut } from "../../schema/locks.js";
6
+ import { editsShut } from "../../schema/protectionState.js";
6
7
  import {
7
8
  hasResolvableClipboardImages,
8
9
  resolveClipboardImages
@@ -222,7 +223,9 @@ function imagePaste() {
222
223
  props: {
223
224
  decorations: (state) => imagePasteKey.getState(state)?.decorations,
224
225
  handlePaste(view, event) {
225
- if (replacementShut(view.state.selection, view.state.doc)) return false;
226
+ if (editsShut(view.state) || replacementShut(view.state.selection, view.state.doc)) {
227
+ return false;
228
+ }
226
229
  if (view.state.selection.ranges.length > 1) return false;
227
230
  const html = event.clipboardData?.getData("text/html") ?? "";
228
231
  if (!hasResolvableClipboardImages(view.dom.ownerDocument, html)) {
@@ -1,5 +1,15 @@
1
1
  /**
2
2
  * Keys not handled here are taken by ProseMirror's base keymap.
3
3
  */
4
- import type { Command } from "prosemirror-state";
4
+ import { type Command, Plugin } from "prosemirror-state";
5
+ /**
6
+ * Keeps undo and redo on their keys where the keymap above cannot reach them.
7
+ *
8
+ * A view that took editing away drops every keydown before the keymap plugin sees it
9
+ * (`view.editable || !(event.type in editHandlers)` in prosemirror-view), so a commenter, who is
10
+ * given no caret, would have no way to take a comment back. A DOM handler is asked ahead of that
11
+ * check. An editable view leaves the keys to the keymap plugin, which would otherwise run them a
12
+ * second time.
13
+ */
14
+ export declare function historyKeys(): Plugin;
5
15
  export declare const docxKeymap: Record<string, Command>;
@@ -1,6 +1,8 @@
1
1
  // src/editor/plugins/keymap.ts
2
2
  import { chainCommands } from "prosemirror-commands";
3
3
  import { undoInputRule } from "prosemirror-inputrules";
4
+ import { keydownHandler } from "prosemirror-keymap";
5
+ import { Plugin } from "prosemirror-state";
4
6
  import { goToNextCell } from "prosemirror-tables";
5
7
  import { canSplit } from "prosemirror-transform";
6
8
  import { toParagraphFormat } from "../../model/format.js";
@@ -55,6 +57,25 @@ var preserveTableFollowingParagraph = (state) => {
55
57
  const paragraphIndex = $from.index(0);
56
58
  return paragraphIndex > 0 && state.doc.child(paragraphIndex - 1).type.spec.tableRole === "table";
57
59
  };
60
+ var historyKeymap = {
61
+ "Mod-z": undo,
62
+ "Mod-y": redo,
63
+ "Shift-Mod-z": redo
64
+ };
65
+ function historyKeys() {
66
+ const runHistoryKeys = keydownHandler(historyKeymap);
67
+ return new Plugin({
68
+ props: {
69
+ handleDOMEvents: {
70
+ keydown: (view, event) => {
71
+ if (view.editable || !runHistoryKeys(view, event)) return false;
72
+ event.preventDefault();
73
+ return true;
74
+ }
75
+ }
76
+ }
77
+ });
78
+ }
58
79
  var docxKeymap = {
59
80
  Enter: chainCommands(leaveEmptyListItem, splitParagraph),
60
81
  "Shift-Enter": insertLineBreak,
@@ -62,9 +83,7 @@ var docxKeymap = {
62
83
  "Mod-Enter": insertPageBreak,
63
84
  // The paragraph keeps adjacent tables separate without changing imported documents.
64
85
  Backspace: chainCommands(preserveTableFollowingParagraph, undoInputRule),
65
- "Mod-z": undo,
66
- "Mod-y": redo,
67
- "Shift-Mod-z": redo,
86
+ ...historyKeymap,
68
87
  "Mod-b": toggleBold,
69
88
  "Mod-i": toggleItalic,
70
89
  "Mod-u": toggleUnderline,
@@ -90,5 +109,6 @@ var docxKeymap = {
90
109
  "Shift-ArrowRight": moveAcrossTab("right", true)
91
110
  };
92
111
  export {
93
- docxKeymap
112
+ docxKeymap,
113
+ historyKeys
94
114
  };
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Rejects transactions disallowed by `schema/locks`. A rejected IME edit also ends the browser
3
- * composition on the next frame so ProseMirror cannot remain stuck in composing state.
2
+ * Rejects transactions disallowed by the guard in `schema/locks`, which answers for the locks the
3
+ * document carries and for the protection the editor runs under (`schema/protection`) alike. A
4
+ * rejected IME edit also ends the browser composition on the next frame so ProseMirror cannot
5
+ * remain stuck in composing state.
4
6
  */
5
7
  import { Plugin } from "prosemirror-state";
6
8
  export declare function lockedContent(): Plugin;
@@ -30,7 +30,7 @@ function lockedContent() {
30
30
  };
31
31
  },
32
32
  filterTransaction(tr, state) {
33
- const allowed = transactionAllowed(tr, state.doc);
33
+ const allowed = transactionAllowed(tr, state);
34
34
  if (!allowed) afterRefusal();
35
35
  return allowed;
36
36
  }
@@ -4,6 +4,7 @@ import {
4
4
  PluginKey,
5
5
  TextSelection
6
6
  } from "prosemirror-state";
7
+ import { editsShut } from "../../schema/protectionState.js";
7
8
  var CLOSE = "close";
8
9
  var menuKey = new PluginKey("docxEditorTableMenu");
9
10
  function isRecord(value) {
@@ -67,7 +68,7 @@ function tableContextMenu() {
67
68
  props: {
68
69
  handleDOMEvents: {
69
70
  contextmenu(view, event) {
70
- if (!view.editable) return false;
71
+ if (editsShut(view.state)) return false;
71
72
  const cellPos = cellAtEvent(view, event.target);
72
73
  if (cellPos === null) return false;
73
74
  event.preventDefault();
@@ -5,6 +5,7 @@ import {
5
5
  TextSelection
6
6
  } from "prosemirror-state";
7
7
  import { CellSelection } from "prosemirror-tables";
8
+ import { editingProtection, editsShut } from "../../schema/protectionState.js";
8
9
  var CLOSE = "close";
9
10
  var menuKey = new PluginKey("docxEditorTextMenu");
10
11
  function isRecord(value) {
@@ -44,6 +45,7 @@ function isInSelection(state, spot) {
44
45
  );
45
46
  }
46
47
  function forTableMenu(view, target, spot) {
48
+ if (editsShut(view.state)) return false;
47
49
  const aboutCells = view.state.selection instanceof CellSelection || !isInSelection(view.state, spot);
48
50
  return aboutCells && isInTableCell(view, target);
49
51
  }
@@ -71,9 +73,12 @@ function textContextMenu() {
71
73
  props: {
72
74
  handleDOMEvents: {
73
75
  contextmenu(view, event) {
74
- if (!view.editable) return false;
76
+ if (editingProtection(view.state) === "readOnly") return false;
75
77
  if (!isInEditor(view, event.target)) return false;
76
78
  const spot = clickedSpot(view, event);
79
+ if (editsShut(view.state) && !isInSelection(view.state, spot)) {
80
+ return false;
81
+ }
77
82
  if (forTableMenu(view, event.target, spot)) return false;
78
83
  event.preventDefault();
79
84
  view.dispatch(openMenu(view.state, event, spot));