@portone/docx-editor 0.1.1 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +17 -5
- package/dist/DocxEditor.d.ts +34 -10
- package/dist/DocxEditor.js +69 -24
- package/dist/core.d.ts +2 -0
- package/dist/core.js +2 -0
- package/dist/docx/commentOnlyChange.d.ts +45 -0
- package/dist/docx/commentOnlyChange.js +145 -0
- package/dist/docx/comments/constants.d.ts +8 -0
- package/dist/docx/comments/constants.js +6 -0
- package/dist/docx/comments/contentTypes.d.ts +7 -0
- package/dist/docx/comments/contentTypes.js +38 -0
- package/dist/docx/comments/model.d.ts +2 -0
- package/dist/docx/comments/model.js +3 -0
- package/dist/docx/comments/people.d.ts +39 -0
- package/dist/docx/comments/people.js +198 -0
- package/dist/docx/comments/reading.d.ts +16 -1
- package/dist/docx/comments/reading.js +15 -5
- package/dist/docx/comments/writing.d.ts +4 -1
- package/dist/docx/comments/writing.js +12 -32
- package/dist/docx/importParagraph.js +1 -0
- package/dist/editor/commands/breakCommands.js +4 -1
- package/dist/editor/commands/canRunCommand.d.ts +3 -2
- package/dist/editor/commands/canRunCommand.js +1 -1
- package/dist/editor/commands/comments/editing.js +6 -4
- package/dist/editor/commands/comments/model.d.ts +9 -0
- package/dist/editor/commands/comments/reading.d.ts +7 -0
- package/dist/editor/commands/comments/reading.js +14 -0
- package/dist/editor/commands/formatting/editing.js +6 -1
- package/dist/editor/commands/historyCommands.js +14 -6
- package/dist/editor/commands/index.d.ts +7 -1
- package/dist/editor/commands/index.js +4 -0
- package/dist/editor/commands/linkCommands.js +2 -0
- package/dist/editor/commands/lockCommands.js +2 -0
- package/dist/editor/commands/tabCommands.js +2 -1
- package/dist/editor/createEditor.d.ts +13 -3
- package/dist/editor/createEditor.js +21 -7
- package/dist/editor/insertImage.js +4 -1
- package/dist/editor/insertTable.js +2 -1
- package/dist/editor/paragraphEdits.d.ts +2 -0
- package/dist/editor/paragraphEdits.js +2 -0
- package/dist/editor/plugins/documentProtection.d.ts +21 -0
- package/dist/editor/plugins/documentProtection.js +44 -0
- package/dist/editor/plugins/imagePaste.js +4 -1
- package/dist/editor/plugins/keymap.d.ts +11 -1
- package/dist/editor/plugins/keymap.js +24 -4
- package/dist/editor/plugins/lockedContent.d.ts +4 -2
- package/dist/editor/plugins/lockedContent.js +1 -1
- package/dist/editor/plugins/tableContextMenu.js +2 -1
- package/dist/editor/plugins/textContextMenu.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/schema/docxSchema.js +5 -0
- package/dist/schema/locks.d.ts +31 -3
- package/dist/schema/locks.js +54 -3
- package/dist/schema/protection.d.ts +77 -0
- package/dist/schema/protection.js +170 -0
- package/dist/schema/protectionState.d.ts +20 -0
- package/dist/schema/protectionState.js +37 -0
- package/dist/table/cellFormatting.js +3 -1
- package/dist/table/commands.js +1 -1
- package/dist/table/merge.js +2 -2
- package/dist/ui/CommentsPanel.d.ts +3 -3
- package/dist/ui/CommentsPanel.js +26 -18
- package/dist/ui/LinkCard.d.ts +1 -2
- package/dist/ui/LinkCard.js +3 -4
- package/dist/ui/TextMenu.js +15 -10
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @portone/docx-editor
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#45](https://github.com/portone-io/docx-editor/pull/45) [`3190a35`](https://github.com/portone-io/docx-editor/commit/3190a359e0686bdc14166e22b1d96c761dabe251) Thanks [@Deea222](https://github.com/Deea222)! - Add a `comment` mode and record who wrote each comment.
|
|
8
|
+
|
|
9
|
+
**Breaking:** `mode` is now required and the `commentAuthor` prop is removed.
|
|
10
|
+
`author: { id, name, initials? }` moves into `mode` for its `comment` and `edit` kinds.
|
|
11
|
+
`contextMenus` moves off `mode` onto `DocxEditor` itself, so `mode: { kind: "edit", contextMenus: false }` becomes `contextMenus={false}`.
|
|
12
|
+
|
|
13
|
+
In `comment` mode the text can be selected and copied but not changed, while comments can be written, answered, resolved, and reopened.
|
|
14
|
+
A comment records its author's identity in the document, so two authors sharing a display name stay distinct.
|
|
15
|
+
Only a comment's author may edit or delete it, and anyone may reply, resolve, and reopen.
|
|
16
|
+
`mode.editableComments: "all"` opens every comment to a moderator.
|
|
17
|
+
The `canEditComment` and `editingProtection` queries report what the current mode allows.
|
|
18
|
+
A server can check a file coming back with `onlyCommentsChangedBy` from `@portone/docx-editor/core`, which answers `{ ok: true }` or `{ ok: false, reason }` for whether the file differs in nothing but one author's comments.
|
|
19
|
+
|
|
3
20
|
## 0.1.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,21 +18,33 @@ import "@portone/docx-editor/styles.css";
|
|
|
18
18
|
import { DocxEditor, type DocxEditorHandle } from "@portone/docx-editor";
|
|
19
19
|
import { useRef } from "react";
|
|
20
20
|
|
|
21
|
-
export function Editor({
|
|
21
|
+
export function Editor({
|
|
22
|
+
file,
|
|
23
|
+
user,
|
|
24
|
+
}: {
|
|
25
|
+
file: File;
|
|
26
|
+
user: { id: string; name: string };
|
|
27
|
+
}) {
|
|
22
28
|
const editorRef = useRef<DocxEditorHandle | null>(null);
|
|
23
29
|
|
|
24
|
-
return
|
|
30
|
+
return (
|
|
31
|
+
<DocxEditor
|
|
32
|
+
ref={editorRef}
|
|
33
|
+
document={file}
|
|
34
|
+
mode={{ kind: "edit", author: { id: user.id, name: user.name } }}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
25
37
|
}
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
`document` accepts a `File`, `Blob`, `ArrayBuffer`, or `Uint8Array`; use the ref to export the edited document as bytes.
|
|
40
|
+
`document` accepts a `File`, `Blob`, `ArrayBuffer`, or `Uint8Array`; `mode` selects read-only, comment-only, or full editing and names the author new comments are attributed to; use the ref to export the edited document as bytes.
|
|
29
41
|
|
|
30
42
|
## What it does
|
|
31
43
|
|
|
32
|
-
- Provides browser editing with built-in controls, read-only
|
|
44
|
+
- Provides browser editing with built-in controls, read-only and comment-only modes, and document locking.
|
|
33
45
|
- Fits the page to the available width by default and lets readers choose a fixed zoom level.
|
|
34
46
|
- Exports edited documents as DOCX bytes or browser downloads.
|
|
35
|
-
- Preserves document structures and package parts across edits.
|
|
47
|
+
- Preserves document structures and package parts across edits, and records who wrote each comment.
|
|
36
48
|
- Reads document styles, theme fonts, page layout, and CJK font information for display.
|
|
37
49
|
|
|
38
50
|
See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build [custom controls](https://docx-editor.portone.io/docs/custom-controls) or use [programmatic DOCX import and export](https://docx-editor.portone.io/docs/core).
|
package/dist/DocxEditor.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { type CSSProperties, type ReactNode } from "react";
|
|
|
13
13
|
import { type DocxSource } from "./docx/importDocx";
|
|
14
14
|
import { type CommentAuthor } from "./editor/commands/commentCommands";
|
|
15
15
|
import { DocxImportError } from "./ooxml/errors";
|
|
16
|
+
import type { EditableComments } from "./schema/protection";
|
|
16
17
|
import type { FontFallbacks } from "./styles/fontStack";
|
|
17
18
|
import type { DocxEditorPresets } from "./ui/presets";
|
|
18
19
|
import { type DocxEditorZoom } from "./ui/zoom";
|
|
@@ -24,22 +25,37 @@ export interface DocxEditorHandle {
|
|
|
24
25
|
/**
|
|
25
26
|
* What the editor is for, which decides what it offers.
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* The three kinds are the standings OOXML document protection names (`ST_DocProtect`) and the
|
|
29
|
+
* ones a shared document gives its readers: a reader, a commenter, an editor. A read-only editor
|
|
30
|
+
* takes no edits, so it has no toolbar and no right click menus either: what used to be three
|
|
31
|
+
* booleans of which one silently emptied the other two is one choice here. A `comment` editor is
|
|
32
|
+
* the `comments` protection - the body may not be changed, comments may be written, answered and
|
|
33
|
+
* settled - which is what a reviewer who must not touch the text is handed. Both kinds that write
|
|
34
|
+
* comments name whose they are, so a reader is never asked for an identity it has no use for.
|
|
35
|
+
*
|
|
36
|
+
* `editableComments` is whose comments the panel offers to edit or delete: one's own, which is a
|
|
37
|
+
* comment carrying no recognised identity or the very identity given here, or every one, which
|
|
38
|
+
* is a moderator's standing. Replying and resolving are open to every commenter either way.
|
|
39
|
+
*
|
|
29
40
|
* `locking` is what a screen where a template is written gets: settling a part of a document is
|
|
30
41
|
* an authoring act, not something every reader of a form should be handed. A lock the document
|
|
31
42
|
* already carries holds whichever mode is chosen.
|
|
32
43
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
44
|
+
* The kind, the author and `editableComments` are read on every render and take effect on the
|
|
45
|
+
* open document. Whether the right click opens the editor's own menus is `contextMenus`, a prop
|
|
46
|
+
* of its own, since the plugins behind it are read when the editor mounts rather than per render.
|
|
36
47
|
*/
|
|
37
48
|
export type DocxEditorMode = {
|
|
38
49
|
kind: "readOnly";
|
|
50
|
+
} | {
|
|
51
|
+
kind: "comment";
|
|
52
|
+
author: CommentAuthor;
|
|
53
|
+
editableComments?: EditableComments;
|
|
39
54
|
} | {
|
|
40
55
|
kind: "edit";
|
|
56
|
+
author: CommentAuthor;
|
|
57
|
+
editableComments?: EditableComments;
|
|
41
58
|
toolbar?: boolean;
|
|
42
|
-
contextMenus?: boolean;
|
|
43
59
|
locking?: boolean;
|
|
44
60
|
};
|
|
45
61
|
export interface DocxEditorProps {
|
|
@@ -58,8 +74,8 @@ export interface DocxEditorProps {
|
|
|
58
74
|
* never silent either way. Hand one in to write the refusal in your own words.
|
|
59
75
|
*/
|
|
60
76
|
renderImportError?: (error: DocxImportError) => ReactNode;
|
|
61
|
-
/** What the editor is for
|
|
62
|
-
mode
|
|
77
|
+
/** What the editor is for: a reader's, a commenter's or an editor's surface */
|
|
78
|
+
mode: DocxEditorMode;
|
|
63
79
|
/** Whether to draw approximate page boundaries over the document's own paper. Drawn when read only too */
|
|
64
80
|
showPageGuides?: boolean;
|
|
65
81
|
/**
|
|
@@ -96,8 +112,6 @@ export interface DocxEditorProps {
|
|
|
96
112
|
* takes effect without a remount.
|
|
97
113
|
*/
|
|
98
114
|
presets?: DocxEditorPresets;
|
|
99
|
-
/** The identity written by the built-in comment and reply composers */
|
|
100
|
-
commentAuthor?: CommentAuthor;
|
|
101
115
|
/**
|
|
102
116
|
* ProseMirror plugins handed in by the consumer, which is how mentions, highlights,
|
|
103
117
|
* autocomplete and the like are added from outside the package.
|
|
@@ -110,6 +124,16 @@ export interface DocxEditorProps {
|
|
|
110
124
|
* `key` and remount.
|
|
111
125
|
*/
|
|
112
126
|
plugins?: readonly Plugin[];
|
|
127
|
+
/**
|
|
128
|
+
* Whether the right click opens the editor's own menus. `false` leaves it to the browser,
|
|
129
|
+
* which is what a consumer drawing menus of its own wants. Defaults to `true`.
|
|
130
|
+
*
|
|
131
|
+
* Read once, when the editor mounts, like `plugins`: the plugins that take the browser's menu
|
|
132
|
+
* away go into the editor state, which is built there. A menu the protection has nothing to
|
|
133
|
+
* offer from stands down by itself, so a mode change needs nothing here. Later changes are
|
|
134
|
+
* ignored; to turn the menus around, change the `key` and remount.
|
|
135
|
+
*/
|
|
136
|
+
contextMenus?: boolean;
|
|
113
137
|
className?: string;
|
|
114
138
|
style?: CSSProperties;
|
|
115
139
|
onReady?: (view: EditorView) => void;
|
package/dist/DocxEditor.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./editor/commands/commentCommands.js";
|
|
16
16
|
import { activeLinkSpan } from "./editor/commands/linkCommands.js";
|
|
17
17
|
import { createEditorState, createEditorView } from "./editor/createEditor.js";
|
|
18
|
+
import { setProtection } from "./editor/plugins/documentProtection.js";
|
|
18
19
|
import { isLinkPanelOpen } from "./editor/plugins/linkPanel.js";
|
|
19
20
|
import { tableMenuAnchor } from "./editor/plugins/tableContextMenu.js";
|
|
20
21
|
import { textMenuAnchor } from "./editor/plugins/textContextMenu.js";
|
|
@@ -23,6 +24,7 @@ import { DocxImportError } from "./ooxml/errors.js";
|
|
|
23
24
|
import { PageGuides } from "./page/PageGuides.js";
|
|
24
25
|
import { A4_PAGE_PIXELS, pagePixels } from "./page/pageLayout.js";
|
|
25
26
|
import { usePageLayout } from "./page/usePageLayout.js";
|
|
27
|
+
import { editingProtection, protectionOf } from "./schema/protectionState.js";
|
|
26
28
|
import { editorClassNames } from "./styles/classNames.js";
|
|
27
29
|
import { CommentsPanel } from "./ui/CommentsPanel.js";
|
|
28
30
|
import { LinkCard } from "./ui/LinkCard.js";
|
|
@@ -34,6 +36,38 @@ import { Toolbar } from "./ui/Toolbar.js";
|
|
|
34
36
|
import { useFitWidthZoom } from "./ui/useFitWidthZoom.js";
|
|
35
37
|
import { normalizeZoom } from "./ui/zoom.js";
|
|
36
38
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
39
|
+
function affordancesOf(mode) {
|
|
40
|
+
switch (mode.kind) {
|
|
41
|
+
case "readOnly":
|
|
42
|
+
return {
|
|
43
|
+
protection: "readOnly",
|
|
44
|
+
author: null,
|
|
45
|
+
editableComments: "own",
|
|
46
|
+
toolbar: false,
|
|
47
|
+
locking: false
|
|
48
|
+
};
|
|
49
|
+
case "comment":
|
|
50
|
+
return {
|
|
51
|
+
protection: "comments",
|
|
52
|
+
author: mode.author ?? null,
|
|
53
|
+
editableComments: mode.editableComments ?? "own",
|
|
54
|
+
toolbar: false,
|
|
55
|
+
locking: false
|
|
56
|
+
};
|
|
57
|
+
case "edit":
|
|
58
|
+
return {
|
|
59
|
+
protection: "none",
|
|
60
|
+
author: mode.author ?? null,
|
|
61
|
+
editableComments: mode.editableComments ?? "own",
|
|
62
|
+
toolbar: mode.toolbar ?? true,
|
|
63
|
+
locking: mode.locking ?? false
|
|
64
|
+
};
|
|
65
|
+
default: {
|
|
66
|
+
const unmodelled = mode;
|
|
67
|
+
return unmodelled;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
37
71
|
function openDocument(bytes) {
|
|
38
72
|
try {
|
|
39
73
|
const { doc, session } = importDocx(bytes);
|
|
@@ -97,32 +131,28 @@ function zoomVariable(factor) {
|
|
|
97
131
|
function DocxEditorSurface({
|
|
98
132
|
document: source,
|
|
99
133
|
renderImportError,
|
|
100
|
-
mode
|
|
134
|
+
mode,
|
|
101
135
|
showPageGuides = true,
|
|
102
136
|
zoom,
|
|
103
137
|
defaultZoom = "fit-width",
|
|
104
138
|
onZoomChange,
|
|
105
139
|
fontFallbacks,
|
|
106
140
|
presets,
|
|
107
|
-
commentAuthor = { name: "Anonymous" },
|
|
108
141
|
plugins,
|
|
142
|
+
contextMenus,
|
|
109
143
|
className,
|
|
110
144
|
style,
|
|
111
145
|
onReady,
|
|
112
146
|
onChange
|
|
113
147
|
}, ref) {
|
|
114
|
-
const
|
|
115
|
-
const showToolbar = mode.kind === "edit" && (mode.toolbar ?? true);
|
|
116
|
-
const allowLocking = mode.kind === "edit" && (mode.locking ?? false);
|
|
148
|
+
const { protection, author, editableComments, toolbar, locking } = affordancesOf(mode);
|
|
117
149
|
const bytes = useDocumentBytes(source);
|
|
118
150
|
const opened = useMemo(
|
|
119
151
|
() => bytes === null ? null : openDocument(bytes),
|
|
120
152
|
[bytes]
|
|
121
153
|
);
|
|
122
154
|
const mountedPlugins = useRef(plugins).current;
|
|
123
|
-
const mountedContextMenus = useRef(
|
|
124
|
-
mode.kind !== "edit" || (mode.contextMenus ?? true)
|
|
125
|
-
).current;
|
|
155
|
+
const mountedContextMenus = useRef(contextMenus ?? true).current;
|
|
126
156
|
const mountedFontFallbacks = useRef(fontFallbacks).current;
|
|
127
157
|
const layerRef = useRef(null);
|
|
128
158
|
const rootRef = useRef(null);
|
|
@@ -173,11 +203,13 @@ function DocxEditorSurface({
|
|
|
173
203
|
...opened.session.comments.extendedOrdered.map(
|
|
174
204
|
(extension) => extension.paraId
|
|
175
205
|
)
|
|
176
|
-
]
|
|
206
|
+
],
|
|
207
|
+
protection,
|
|
208
|
+
author,
|
|
209
|
+
editableComments
|
|
177
210
|
}),
|
|
178
211
|
defaults: opened.session.defaults,
|
|
179
212
|
geometry: opened.session.geometry,
|
|
180
|
-
readOnly,
|
|
181
213
|
fontFallbacks: mountedFontFallbacks,
|
|
182
214
|
onStateChange: (state) => {
|
|
183
215
|
keptState.current = { of: opened, state };
|
|
@@ -196,19 +228,32 @@ function DocxEditorSurface({
|
|
|
196
228
|
};
|
|
197
229
|
}, [
|
|
198
230
|
opened,
|
|
199
|
-
readOnly,
|
|
200
231
|
mountedContextMenus,
|
|
201
232
|
mountedFontFallbacks,
|
|
202
233
|
mountedPlugins,
|
|
203
234
|
latestOnReady,
|
|
204
235
|
latestOnChange
|
|
205
236
|
]);
|
|
237
|
+
const authorId = author?.id ?? null;
|
|
238
|
+
const mounted = live !== null;
|
|
239
|
+
useLayoutEffect(() => {
|
|
240
|
+
if (protection === "readOnly") setCommentComposerOpen(false);
|
|
241
|
+
const view = viewRef.current;
|
|
242
|
+
if (!view || !mounted) return;
|
|
243
|
+
const held = protectionOf(view.state);
|
|
244
|
+
if (held.protection === protection && held.authorId === authorId && held.editableComments === editableComments) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
view.dispatch(
|
|
248
|
+
setProtection(view.state.tr, { protection, author, editableComments })
|
|
249
|
+
);
|
|
250
|
+
}, [mounted, protection, authorId, editableComments]);
|
|
206
251
|
useImperativeHandle(ref, () => {
|
|
207
252
|
const view = viewRef.current;
|
|
208
253
|
if (!view || opened?.status !== "opened") return null;
|
|
209
254
|
const session = opened.session;
|
|
210
255
|
return { view, exportBytes: () => exportDocx(view.state.doc, session) };
|
|
211
|
-
}, [opened
|
|
256
|
+
}, [opened]);
|
|
212
257
|
const overlay = usePageLayout({
|
|
213
258
|
view: live?.view ?? null,
|
|
214
259
|
layer: layerRef,
|
|
@@ -219,21 +264,22 @@ function DocxEditorSurface({
|
|
|
219
264
|
if (opened?.status === "rejected") {
|
|
220
265
|
return renderImportError ? renderImportError(opened.error) : /* @__PURE__ */ jsx(ImportRejection, { error: opened.error });
|
|
221
266
|
}
|
|
222
|
-
const
|
|
223
|
-
const
|
|
267
|
+
const bodyOpen = live !== null && editingProtection(live.state) === "none";
|
|
268
|
+
const commentsOpenToWrite = live !== null && editingProtection(live.state) !== "readOnly";
|
|
269
|
+
const textAnchor = live ? textMenuAnchor(live.state) : null;
|
|
270
|
+
const tableAnchor = live && !textAnchor ? tableMenuAnchor(live.state) : null;
|
|
224
271
|
const linkAtCursor = live && !live.view.composing && !isLinkPanelOpen(live.state) ? activeLinkSpan(live.state) : null;
|
|
225
272
|
const comments = live?.state === void 0 ? [] : documentComments(live.state);
|
|
226
273
|
const hasUnresolvedComments = comments.some((comment) => !comment.resolved);
|
|
227
|
-
const effectiveComposerOpen = commentComposerOpen &&
|
|
274
|
+
const effectiveComposerOpen = commentComposerOpen && commentsOpenToWrite;
|
|
228
275
|
const showComments = live !== null && (commentsOpen || effectiveComposerOpen || hasUnresolvedComments);
|
|
229
276
|
const commentsPanel = live && showComments && /* @__PURE__ */ jsx(
|
|
230
277
|
CommentsPanel,
|
|
231
278
|
{
|
|
232
279
|
view: live.view,
|
|
233
280
|
state: live.state,
|
|
234
|
-
readOnly,
|
|
235
281
|
composerOpen: effectiveComposerOpen,
|
|
236
|
-
author
|
|
282
|
+
author,
|
|
237
283
|
closeComposer: () => setCommentComposerOpen(false),
|
|
238
284
|
scrollContainer: rootRef.current,
|
|
239
285
|
allCommentsOpen: commentsOpen
|
|
@@ -245,7 +291,7 @@ function DocxEditorSurface({
|
|
|
245
291
|
className: [editorClassNames.frame, className].filter(Boolean).join(" "),
|
|
246
292
|
style,
|
|
247
293
|
children: [
|
|
248
|
-
live &&
|
|
294
|
+
live && toolbar && /* @__PURE__ */ jsx(
|
|
249
295
|
Toolbar,
|
|
250
296
|
{
|
|
251
297
|
view: live.view,
|
|
@@ -270,7 +316,7 @@ function DocxEditorSurface({
|
|
|
270
316
|
"data-comments": showComments ? "visible" : void 0,
|
|
271
317
|
style: zoomVariable(effectiveZoom),
|
|
272
318
|
children: [
|
|
273
|
-
live &&
|
|
319
|
+
live && !bodyOpen && comments.length > 0 && /* @__PURE__ */ jsx(
|
|
274
320
|
"button",
|
|
275
321
|
{
|
|
276
322
|
type: "button",
|
|
@@ -306,14 +352,13 @@ function DocxEditorSurface({
|
|
|
306
352
|
]
|
|
307
353
|
}
|
|
308
354
|
),
|
|
309
|
-
live &&
|
|
355
|
+
live && bodyOpen && isLinkPanelOpen(live.state) && /* @__PURE__ */ jsx(LinkPanel, { view: live.view, state: live.state }),
|
|
310
356
|
live && linkAtCursor && /* @__PURE__ */ jsx(
|
|
311
357
|
LinkCard,
|
|
312
358
|
{
|
|
313
359
|
view: live.view,
|
|
314
360
|
state: live.state,
|
|
315
|
-
link: linkAtCursor
|
|
316
|
-
readOnly
|
|
361
|
+
link: linkAtCursor
|
|
317
362
|
},
|
|
318
363
|
linkAtCursor.from
|
|
319
364
|
),
|
|
@@ -323,7 +368,7 @@ function DocxEditorSurface({
|
|
|
323
368
|
view: live.view,
|
|
324
369
|
state: live.state,
|
|
325
370
|
anchor: textAnchor,
|
|
326
|
-
allowLocking,
|
|
371
|
+
allowLocking: locking,
|
|
327
372
|
onAddComment: () => {
|
|
328
373
|
setCommentComposerOpen(true);
|
|
329
374
|
}
|
|
@@ -335,7 +380,7 @@ function DocxEditorSurface({
|
|
|
335
380
|
view: live.view,
|
|
336
381
|
state: live.state,
|
|
337
382
|
anchor: tableAnchor,
|
|
338
|
-
allowLocking
|
|
383
|
+
allowLocking: locking
|
|
339
384
|
}
|
|
340
385
|
)
|
|
341
386
|
]
|
package/dist/core.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { Node as PMNode } from "prosemirror-model";
|
|
6
6
|
import { type DocxBytes } from "./docx/importDocx";
|
|
7
7
|
import type { DocxSession } from "./docx/session";
|
|
8
|
+
export type { CommentOnlyVerdict } from "./docx/commentOnlyChange";
|
|
9
|
+
export { onlyCommentsChangedBy } from "./docx/commentOnlyChange";
|
|
8
10
|
export { exportDocx } from "./docx/exportDocx";
|
|
9
11
|
export type { ParagraphStyleOption } from "./docx/formatting";
|
|
10
12
|
export type { DocxBytes } from "./docx/importDocx";
|
package/dist/core.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/core.ts
|
|
2
2
|
import { importDocx as openDocx } from "./docx/importDocx.js";
|
|
3
|
+
import { onlyCommentsChangedBy } from "./docx/commentOnlyChange.js";
|
|
3
4
|
import { exportDocx } from "./docx/exportDocx.js";
|
|
4
5
|
import { documentNumbering, documentPartPath } from "./docx/session.js";
|
|
5
6
|
import {
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
emuToPx,
|
|
27
28
|
exportDocx,
|
|
28
29
|
importDocx,
|
|
30
|
+
onlyCommentsChangedBy,
|
|
29
31
|
parseNumbering,
|
|
30
32
|
pxToEmu,
|
|
31
33
|
toCellFormat,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a file handed back changed in nothing but comments one author is allowed to have made.
|
|
3
|
+
*
|
|
4
|
+
* The editor's own refusal under a comment protection (`schema/protection`) is a courtesy to the
|
|
5
|
+
* user: the browser holds the file, so the rule is held again here, where a server takes it in.
|
|
6
|
+
* The judgement is over the package rather than over the document alone, since a submission is
|
|
7
|
+
* free to rewrite anything the story does not carry - the paper the document is written on, its
|
|
8
|
+
* styles, its headers - and a document comparison would see none of it.
|
|
9
|
+
*/
|
|
10
|
+
import { type EditableComments } from "../schema/protection";
|
|
11
|
+
import { type DocxBytes } from "./importDocx";
|
|
12
|
+
/**
|
|
13
|
+
* Why a file is not the one it claims to be. `part-changed` and `relationship-changed` name the
|
|
14
|
+
* part they were reached over; the other three are about the document story itself.
|
|
15
|
+
*/
|
|
16
|
+
export type CommentOnlyVerdict = {
|
|
17
|
+
ok: true;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
reason: "body-changed" | "comment-not-owned" | "comment-author-forged";
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: "part-changed" | "relationship-changed";
|
|
24
|
+
part: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Whether the submitted file differs from the original in nothing but comments, every one of them
|
|
28
|
+
* added, edited, moved, deleted, replied to or settled by the author with this identity.
|
|
29
|
+
*
|
|
30
|
+
* Every part of the package has to arrive as it left, save for the three a comment is written
|
|
31
|
+
* across and the relationship and content type they are declared with; the document story itself
|
|
32
|
+
* has to read as it did, comments aside. A comment carrying no recorded identity is everyone's to
|
|
33
|
+
* edit here as it is in the editor (`schema/protection`), while a comment that appeared has to
|
|
34
|
+
* carry this identity: a file can claim any author, and the editor's own hand in writing it is
|
|
35
|
+
* not there to vouch for it. An identity already recorded is nobody's to rewrite.
|
|
36
|
+
*
|
|
37
|
+
* `editableComments: "all"` judges the file of an editor opened for a moderator, where every
|
|
38
|
+
* comment was theirs to edit; an identity is nobody's to rewrite under either setting.
|
|
39
|
+
*
|
|
40
|
+
* Bytes that are not a readable docx are turned down the way opening one is, with a
|
|
41
|
+
* `DocxImportError`, rather than being answered as a file that changed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function onlyCommentsChangedBy(original: DocxBytes, submitted: DocxBytes, authorId: string, { editableComments }?: {
|
|
44
|
+
editableComments?: EditableComments;
|
|
45
|
+
}): CommentOnlyVerdict;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/docx/commentOnlyChange.ts
|
|
2
|
+
import { decodeUtf8, elementChildren, parseXml } from "../ooxml/xml.js";
|
|
3
|
+
import {
|
|
4
|
+
changesOnlyComments,
|
|
5
|
+
commentAdditionsBy,
|
|
6
|
+
commentEditsOwned,
|
|
7
|
+
commentIdentitiesKept
|
|
8
|
+
} from "../schema/protection.js";
|
|
9
|
+
import {
|
|
10
|
+
COMMENTS_CONTENT_TYPE,
|
|
11
|
+
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
12
|
+
COMMENTS_EXTENDED_REL_TYPE,
|
|
13
|
+
COMMENTS_REL_TYPE,
|
|
14
|
+
CONTENT_TYPES_PATH,
|
|
15
|
+
PEOPLE_CONTENT_TYPE,
|
|
16
|
+
PEOPLE_REL_TYPE
|
|
17
|
+
} from "./comments/constants.js";
|
|
18
|
+
import { importDocx } from "./importDocx.js";
|
|
19
|
+
import {
|
|
20
|
+
readRelationships,
|
|
21
|
+
relsPathOf,
|
|
22
|
+
resolveTarget
|
|
23
|
+
} from "./relationships.js";
|
|
24
|
+
var COMMENT_REL_TYPES = [
|
|
25
|
+
COMMENTS_REL_TYPE,
|
|
26
|
+
COMMENTS_EXTENDED_REL_TYPE,
|
|
27
|
+
PEOPLE_REL_TYPE
|
|
28
|
+
];
|
|
29
|
+
var COMMENT_CONTENT_TYPES = [
|
|
30
|
+
COMMENTS_CONTENT_TYPE,
|
|
31
|
+
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
32
|
+
PEOPLE_CONTENT_TYPE
|
|
33
|
+
];
|
|
34
|
+
var refused = (reason) => ({ ok: false, reason });
|
|
35
|
+
function sameBytes(before, after) {
|
|
36
|
+
return before.length === after.length && before.every((byte, index) => byte === after[index]);
|
|
37
|
+
}
|
|
38
|
+
function commentPartPaths(session) {
|
|
39
|
+
const related = readRelationships(
|
|
40
|
+
session.parts,
|
|
41
|
+
relsPathOf(session.mainPartPath)
|
|
42
|
+
).filter(
|
|
43
|
+
(entry) => !entry.external && COMMENT_REL_TYPES.includes(entry.type)
|
|
44
|
+
);
|
|
45
|
+
return new Set(
|
|
46
|
+
related.map((entry) => resolveTarget(session.mainPartPath, entry.target))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function aroundTheStory(session) {
|
|
50
|
+
const preserved = session.blocks.filter(
|
|
51
|
+
(block) => block.node.type.name !== "paragraph" && block.node.type.name !== "table"
|
|
52
|
+
).map((block) => block.xml).join("");
|
|
53
|
+
return session.documentPrefix + preserved + session.documentSuffix;
|
|
54
|
+
}
|
|
55
|
+
function relationshipsKept(before, after) {
|
|
56
|
+
const now = new Map(after.map((entry) => [entry.id, entry]));
|
|
57
|
+
const kept = before.every((entry) => {
|
|
58
|
+
const current = now.get(entry.id);
|
|
59
|
+
return current !== void 0 && current.type === entry.type && current.target === entry.target && current.external === entry.external;
|
|
60
|
+
});
|
|
61
|
+
const ids = new Set(before.map((entry) => entry.id));
|
|
62
|
+
return kept && after.every(
|
|
63
|
+
(entry) => ids.has(entry.id) || COMMENT_REL_TYPES.includes(entry.type)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function contentTypes(bytes) {
|
|
67
|
+
if (bytes === void 0) return /* @__PURE__ */ new Map();
|
|
68
|
+
const declared = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const el of elementChildren(
|
|
70
|
+
parseXml(decodeUtf8(bytes).text).documentElement
|
|
71
|
+
)) {
|
|
72
|
+
const key = el.localName === "Default" ? el.getAttribute("Extension") : el.localName === "Override" ? el.getAttribute("PartName") : null;
|
|
73
|
+
if (key !== null) declared.set(key, el.getAttribute("ContentType") ?? "");
|
|
74
|
+
}
|
|
75
|
+
return declared;
|
|
76
|
+
}
|
|
77
|
+
function contentTypesKept(before, after) {
|
|
78
|
+
for (const [key, type] of before) {
|
|
79
|
+
if (after.get(key) !== type) return false;
|
|
80
|
+
}
|
|
81
|
+
for (const [key, type] of after) {
|
|
82
|
+
if (!before.has(key) && !COMMENT_CONTENT_TYPES.includes(type)) return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function packageKept(before, after) {
|
|
87
|
+
if (before.mainPartPath !== after.mainPartPath) {
|
|
88
|
+
return { ok: false, reason: "part-changed", part: before.mainPartPath };
|
|
89
|
+
}
|
|
90
|
+
const relsPath = relsPathOf(before.mainPartPath);
|
|
91
|
+
const untouched = /* @__PURE__ */ new Set([
|
|
92
|
+
before.mainPartPath,
|
|
93
|
+
relsPath,
|
|
94
|
+
CONTENT_TYPES_PATH,
|
|
95
|
+
...commentPartPaths(before),
|
|
96
|
+
...commentPartPaths(after)
|
|
97
|
+
]);
|
|
98
|
+
for (const path of /* @__PURE__ */ new Set([...before.parts.keys(), ...after.parts.keys()])) {
|
|
99
|
+
if (untouched.has(path)) continue;
|
|
100
|
+
const was = before.parts.get(path);
|
|
101
|
+
const now = after.parts.get(path);
|
|
102
|
+
if (was === void 0 || now === void 0 || !sameBytes(was, now)) {
|
|
103
|
+
return { ok: false, reason: "part-changed", part: path };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!relationshipsKept(
|
|
107
|
+
readRelationships(before.parts, relsPath),
|
|
108
|
+
readRelationships(after.parts, relsPath)
|
|
109
|
+
)) {
|
|
110
|
+
return { ok: false, reason: "relationship-changed", part: relsPath };
|
|
111
|
+
}
|
|
112
|
+
if (!contentTypesKept(
|
|
113
|
+
contentTypes(before.parts.get(CONTENT_TYPES_PATH)),
|
|
114
|
+
contentTypes(after.parts.get(CONTENT_TYPES_PATH))
|
|
115
|
+
)) {
|
|
116
|
+
return { ok: false, reason: "part-changed", part: CONTENT_TYPES_PATH };
|
|
117
|
+
}
|
|
118
|
+
if (aroundTheStory(before) !== aroundTheStory(after)) {
|
|
119
|
+
return { ok: false, reason: "part-changed", part: before.mainPartPath };
|
|
120
|
+
}
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
function storyKept(before, after, authorId, editableComments) {
|
|
124
|
+
if (!changesOnlyComments(before, after)) return refused("body-changed");
|
|
125
|
+
if (!commentIdentitiesKept(before, after) || !commentAdditionsBy(before, after, authorId)) {
|
|
126
|
+
return refused("comment-author-forged");
|
|
127
|
+
}
|
|
128
|
+
if (!commentEditsOwned(before, after, {
|
|
129
|
+
protection: "comments",
|
|
130
|
+
authorId,
|
|
131
|
+
editableComments
|
|
132
|
+
})) {
|
|
133
|
+
return refused("comment-not-owned");
|
|
134
|
+
}
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
function onlyCommentsChangedBy(original, submitted, authorId, { editableComments = "own" } = {}) {
|
|
138
|
+
const before = importDocx(original);
|
|
139
|
+
const after = importDocx(submitted);
|
|
140
|
+
const packaged = packageKept(before.session, after.session);
|
|
141
|
+
return packaged.ok ? storyKept(before.doc, after.doc, authorId, editableComments) : packaged;
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
onlyCommentsChangedBy
|
|
145
|
+
};
|
|
@@ -6,3 +6,11 @@ export declare const CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
|
6
6
|
export declare const W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
7
7
|
export declare const W15_NS = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
8
8
|
export declare const MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
9
|
+
export declare const PEOPLE_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/people";
|
|
10
|
+
export declare const PEOPLE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
|
|
11
|
+
/**
|
|
12
|
+
* The `w15:providerId` under which this editor records a comment author's identity. An identity
|
|
13
|
+
* another provider recorded, Word's directory above all, is one this editor cannot vouch for and
|
|
14
|
+
* is read as none.
|
|
15
|
+
*/
|
|
16
|
+
export declare const COMMENT_AUTHOR_PROVIDER = "portone-docx-editor";
|
|
@@ -8,13 +8,19 @@ var CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
|
8
8
|
var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
9
9
|
var W15_NS = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
10
10
|
var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
11
|
+
var PEOPLE_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/people";
|
|
12
|
+
var PEOPLE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
|
|
13
|
+
var COMMENT_AUTHOR_PROVIDER = "portone-docx-editor";
|
|
11
14
|
export {
|
|
12
15
|
COMMENTS_CONTENT_TYPE,
|
|
13
16
|
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
14
17
|
COMMENTS_EXTENDED_REL_TYPE,
|
|
15
18
|
COMMENTS_REL_TYPE,
|
|
19
|
+
COMMENT_AUTHOR_PROVIDER,
|
|
16
20
|
CONTENT_TYPES_PATH,
|
|
17
21
|
MC_NS,
|
|
22
|
+
PEOPLE_CONTENT_TYPE,
|
|
23
|
+
PEOPLE_REL_TYPE,
|
|
18
24
|
W14_NS,
|
|
19
25
|
W15_NS
|
|
20
26
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Declares a part an export adds in `[Content_Types].xml`, leaving the original text as it came. */
|
|
2
|
+
/**
|
|
3
|
+
* The content types part with an override for this part, or null when it already declares one.
|
|
4
|
+
* `current` is the part as an earlier addition in the same export left it, so that two additions
|
|
5
|
+
* do not each write over the other's declaration.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withContentType(parts: Map<string, Uint8Array>, partPath: string, contentType: string, current: Uint8Array | undefined): Uint8Array | null;
|