@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/dist/schema/locks.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Evaluates OOXML content and deletion locks for inline controls and whole table cells. Commands
|
|
3
3
|
* query these predicates before editing; `editor/plugins/lockedContent` enforces them at runtime.
|
|
4
|
+
*
|
|
5
|
+
* `transactionAllowed` is the one guard, and it answers for the protection the editor runs under
|
|
6
|
+
* (`./protection`) as well as for the locks, so that a command asking it asks both.
|
|
4
7
|
*/
|
|
5
8
|
import type { Mark, Node as PMNode } from "prosemirror-model";
|
|
6
|
-
import { PluginKey, type Selection, type Transaction } from "prosemirror-state";
|
|
9
|
+
import { type EditorState, PluginKey, type Selection, type Transaction } from "prosemirror-state";
|
|
10
|
+
import { type ProtectionState } from "./protection";
|
|
7
11
|
/**
|
|
8
12
|
* The pass that lets a transaction through the guard, which is how a lock can be lifted at all.
|
|
9
13
|
* A plugin key is used as the name so that it cannot collide with a consumer's own metadata.
|
|
@@ -99,6 +103,27 @@ export declare function selectionShut(selection: Selection, doc: PMNode): boolea
|
|
|
99
103
|
* it would have gone through.
|
|
100
104
|
*/
|
|
101
105
|
export declare function replacementShut(selection: Selection, doc: PMNode): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Whether any step of the transaction reaches a comment node: puts one in, takes one out, or
|
|
108
|
+
* rewrites the one where it stands, which is how a body, a reply and a resolution change.
|
|
109
|
+
*
|
|
110
|
+
* Every comment lives in its three nodes, so a change that reaches none of them cannot have
|
|
111
|
+
* changed a comment. That is what lets the guard settle the common transaction - typing, and
|
|
112
|
+
* nothing more - over the stretch it rewrote rather than over the whole document.
|
|
113
|
+
*
|
|
114
|
+
* A step of a kind this does not know - one a consumer brought - is answered as reaching one,
|
|
115
|
+
* since what it rewrote is not known either. The whole-document judgement then has the say, and
|
|
116
|
+
* an unknown step costs a comparison rather than a hole in the guard.
|
|
117
|
+
*/
|
|
118
|
+
export declare function transactionTouchesComments(tr: Transaction): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Whether the protection lets this transaction through (`./protection`).
|
|
121
|
+
*
|
|
122
|
+
* The whole-document judgement is reached for only when a comment is touched at all
|
|
123
|
+
* (`transactionTouchesComments`). A change that touches none is a body edit: through under `none`,
|
|
124
|
+
* refused under `comments`, and nothing about ownership to ask.
|
|
125
|
+
*/
|
|
126
|
+
export declare function protectionAllowsTransaction(tr: Transaction, rules: ProtectionState): boolean;
|
|
102
127
|
/**
|
|
103
128
|
* Whether the guard would let this transaction through, decided and nothing else.
|
|
104
129
|
*
|
|
@@ -106,7 +131,10 @@ export declare function replacementShut(selection: Selection, doc: PMNode): bool
|
|
|
106
131
|
* (`editor/plugins/lockedContent`) - which a query about a button's state may not set off, so the
|
|
107
132
|
* decision stands apart from it and every caller building an edit asks this rather than handing
|
|
108
133
|
* the transaction to a state.
|
|
109
|
-
*
|
|
134
|
+
*
|
|
135
|
+
* The protection is judged before the passes and without them: a replayed edit is still an edit,
|
|
136
|
+
* and a pass that lifts a lock lifts no protection. Its judgement is over the whole change rather
|
|
137
|
+
* than step by step, which is what lets it tell a comment from everything else.
|
|
110
138
|
*/
|
|
111
|
-
export declare function transactionAllowed(tr: Transaction,
|
|
139
|
+
export declare function transactionAllowed(tr: Transaction, state: EditorState): boolean;
|
|
112
140
|
export {};
|
package/dist/schema/locks.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/schema/locks.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
PluginKey
|
|
4
|
+
} from "prosemirror-state";
|
|
3
5
|
import {
|
|
4
6
|
AddMarkStep,
|
|
5
7
|
AddNodeMarkStep,
|
|
@@ -10,6 +12,11 @@ import {
|
|
|
10
12
|
ReplaceStep
|
|
11
13
|
} from "prosemirror-transform";
|
|
12
14
|
import { docxSchema } from "./index.js";
|
|
15
|
+
import {
|
|
16
|
+
isCommentNode,
|
|
17
|
+
protectionAllows
|
|
18
|
+
} from "./protection.js";
|
|
19
|
+
import { protectionOf } from "./protectionState.js";
|
|
13
20
|
var unlockAllowed = new PluginKey("docxEditorUnlockAllowed");
|
|
14
21
|
var historyReplay = new PluginKey("docxEditorHistoryReplay");
|
|
15
22
|
var OPEN = { contents: false, deletion: false };
|
|
@@ -188,11 +195,53 @@ function stepAllowed(step, doc) {
|
|
|
188
195
|
function carriesPass(tr) {
|
|
189
196
|
return tr.getMeta(unlockAllowed) === true || tr.getMeta(historyReplay) === true;
|
|
190
197
|
}
|
|
191
|
-
function
|
|
198
|
+
function rangeHoldsComment(doc, from, to) {
|
|
199
|
+
let found = false;
|
|
200
|
+
doc.nodesBetween(from, to, (node) => {
|
|
201
|
+
if (found) return false;
|
|
202
|
+
if (isCommentNode(node)) found = true;
|
|
203
|
+
return !found;
|
|
204
|
+
});
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
function transactionTouchesComments(tr) {
|
|
208
|
+
return tr.steps.some((step, index) => {
|
|
209
|
+
const before = tr.docs[index];
|
|
210
|
+
const after = tr.docs[index + 1] ?? tr.doc;
|
|
211
|
+
if (step instanceof AttrStep || step instanceof AddNodeMarkStep || step instanceof RemoveNodeMarkStep) {
|
|
212
|
+
const node = before.nodeAt(step.pos);
|
|
213
|
+
return node !== null && isCommentNode(node);
|
|
214
|
+
}
|
|
215
|
+
if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep || step instanceof AddMarkStep || step instanceof RemoveMarkStep)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
let touched = false;
|
|
219
|
+
step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
|
|
220
|
+
touched ||= rangeHoldsComment(before, oldStart, oldEnd) || rangeHoldsComment(after, newStart, newEnd);
|
|
221
|
+
});
|
|
222
|
+
return touched;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function protectionAllowsTransaction(tr, rules) {
|
|
226
|
+
switch (rules.protection) {
|
|
227
|
+
case "readOnly":
|
|
228
|
+
return false;
|
|
229
|
+
case "none":
|
|
230
|
+
return !transactionTouchesComments(tr) || protectionAllows(tr.before, tr.doc, rules);
|
|
231
|
+
case "comments":
|
|
232
|
+
return transactionTouchesComments(tr) && protectionAllows(tr.before, tr.doc, rules);
|
|
233
|
+
default: {
|
|
234
|
+
const unmodelled = rules.protection;
|
|
235
|
+
return unmodelled;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function transactionAllowed(tr, state) {
|
|
192
240
|
if (!tr.docChanged) return true;
|
|
241
|
+
if (!protectionAllowsTransaction(tr, protectionOf(state))) return false;
|
|
193
242
|
if (carriesPass(tr)) return true;
|
|
194
243
|
return tr.steps.every(
|
|
195
|
-
(step, index) => stepAllowed(step, tr.docs[index] ?? doc)
|
|
244
|
+
(step, index) => stepAllowed(step, tr.docs[index] ?? state.doc)
|
|
196
245
|
);
|
|
197
246
|
}
|
|
198
247
|
export {
|
|
@@ -203,9 +252,11 @@ export {
|
|
|
203
252
|
insideLockedCell,
|
|
204
253
|
isLockedCell,
|
|
205
254
|
lockedMarkOf,
|
|
255
|
+
protectionAllowsTransaction,
|
|
206
256
|
rangeTouchesLocked,
|
|
207
257
|
replacementShut,
|
|
208
258
|
selectionShut,
|
|
209
259
|
transactionAllowed,
|
|
260
|
+
transactionTouchesComments,
|
|
210
261
|
unlockAllowed
|
|
211
262
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protection an editor runs under: what the document as a whole may receive, over and above
|
|
3
|
+
* the locks its own controls carry (`./locks`).
|
|
4
|
+
*
|
|
5
|
+
* The levels are named after OOXML `ST_DocProtect`, the enumeration `w:documentProtection/@w:edit`
|
|
6
|
+
* takes (ECMA-376 Part 1 §17.15.1.29): `readOnly` takes no edit at all, `comments` takes comments
|
|
7
|
+
* and nothing else, `none` takes every body edit. No level lifts the ownership rule over comments:
|
|
8
|
+
* whose comment may be edited is `editableComments`' question at every level, `none` included.
|
|
9
|
+
* `trackedChanges` and `forms`, the two values of the enumeration not modelled yet, would be added
|
|
10
|
+
* here.
|
|
11
|
+
*
|
|
12
|
+
* The protection is editor state rather than document state: it is the reader's standing, not the
|
|
13
|
+
* file's, so nothing here is written to or read from OOXML. `editor/plugins/documentProtection`
|
|
14
|
+
* holds it in the state and `./protectionState` reads it back; the guard (`transactionAllowed` in
|
|
15
|
+
* `./locks`) asks the judgements here. They take documents alone, so that the `./core` entry can
|
|
16
|
+
* make the same judgement over a file without an editor.
|
|
17
|
+
*/
|
|
18
|
+
import { type Node as PMNode } from "prosemirror-model";
|
|
19
|
+
export type EditingProtection = "none" | "readOnly" | "comments";
|
|
20
|
+
/**
|
|
21
|
+
* Whose comments may be edited or deleted. `own` is a comment carrying no recognised identity, or
|
|
22
|
+
* the very identity comments are being written under; `all` is every comment there is, which is
|
|
23
|
+
* the standing of a moderator.
|
|
24
|
+
*/
|
|
25
|
+
export type EditableComments = "own" | "all";
|
|
26
|
+
export interface ProtectionState {
|
|
27
|
+
protection: EditingProtection;
|
|
28
|
+
/** The identity comments are written under. Null where nobody is writing any, a reader above all */
|
|
29
|
+
authorId: string | null;
|
|
30
|
+
editableComments: EditableComments;
|
|
31
|
+
}
|
|
32
|
+
/** Whether this node is one of the three a comment stands in the story as */
|
|
33
|
+
export declare function isCommentNode(node: PMNode): boolean;
|
|
34
|
+
/** Whether the two documents differ in nothing but their comments */
|
|
35
|
+
export declare function changesOnlyComments(before: PMNode, after: PMNode): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a comment written under this identity may be edited or deleted.
|
|
38
|
+
*
|
|
39
|
+
* A comment carrying no recognised identity belongs to nobody in particular and stays open to
|
|
40
|
+
* everyone, which is what every comment was before identities were recorded and what a comment
|
|
41
|
+
* made in Word is; one carrying an identity is its author's alone.
|
|
42
|
+
*/
|
|
43
|
+
export declare function commentOwned(rules: ProtectionState, authorId: string | null): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether every comment and reply that was already there kept the identity it was written under.
|
|
46
|
+
*
|
|
47
|
+
* An identity is not an edit anyone may make, its own author included: a comment rewritten into
|
|
48
|
+
* another author's name would be that author's to edit from then on, and one rewritten into
|
|
49
|
+
* nobody's would be everyone's. Only a comment that appears carries an identity chosen for it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function commentIdentitiesKept(before: PMNode, after: PMNode): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether every comment edited, moved or taken away between the two documents belonged to whoever
|
|
54
|
+
* did it. Adding a comment, replying, and settling a thread as resolved or open belong to
|
|
55
|
+
* everyone.
|
|
56
|
+
*
|
|
57
|
+
* Moving a comment onto other text is an edit of that comment rather than of the body, since the
|
|
58
|
+
* body reads the same afterwards, and it is the owner's to make.
|
|
59
|
+
* A root taken away takes its replies with it whoever wrote them: a thread hangs off its root,
|
|
60
|
+
* and the root's owner may take the thread down.
|
|
61
|
+
*/
|
|
62
|
+
export declare function commentEditsOwned(before: PMNode, after: PMNode, rules: ProtectionState): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the protection lets a document go from `before` to `after`.
|
|
65
|
+
*
|
|
66
|
+
* Ownership is judged only over a change that is nothing but comments. A body edit that sweeps a
|
|
67
|
+
* comment away with the text it was anchored to is a body edit, and the standing to make one is
|
|
68
|
+
* the protection's question; under `comments` no such edit goes through in the first place.
|
|
69
|
+
*/
|
|
70
|
+
export declare function protectionAllows(before: PMNode, after: PMNode, rules: ProtectionState): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Whether every comment and reply that appeared between the two documents was written under this
|
|
73
|
+
* identity, and every one that was already there kept the identity it was written under. The
|
|
74
|
+
* editor takes an addition from anyone, since it writes the author itself; a server taking a file
|
|
75
|
+
* back does not, since the file could claim any author.
|
|
76
|
+
*/
|
|
77
|
+
export declare function commentAdditionsBy(before: PMNode, after: PMNode, authorId: string): boolean;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/schema/protection.ts
|
|
2
|
+
import { Fragment } from "prosemirror-model";
|
|
3
|
+
var COMMENT_NODES = /* @__PURE__ */ new Set([
|
|
4
|
+
"commentStart",
|
|
5
|
+
"commentEnd",
|
|
6
|
+
"commentReference"
|
|
7
|
+
]);
|
|
8
|
+
function isCommentNode(node) {
|
|
9
|
+
return COMMENT_NODES.has(node.type.name);
|
|
10
|
+
}
|
|
11
|
+
function withoutComments(node) {
|
|
12
|
+
if (node.isLeaf) return node;
|
|
13
|
+
const kept = [];
|
|
14
|
+
node.forEach((child) => {
|
|
15
|
+
if (!isCommentNode(child)) kept.push(withoutComments(child));
|
|
16
|
+
});
|
|
17
|
+
return node.copy(Fragment.fromArray(kept));
|
|
18
|
+
}
|
|
19
|
+
function changesOnlyComments(before, after) {
|
|
20
|
+
return withoutComments(before).eq(withoutComments(after));
|
|
21
|
+
}
|
|
22
|
+
function stringOrNull(value) {
|
|
23
|
+
return typeof value === "string" ? value : null;
|
|
24
|
+
}
|
|
25
|
+
function bodyOf(attrs) {
|
|
26
|
+
return {
|
|
27
|
+
authorId: stringOrNull(attrs.authorId),
|
|
28
|
+
author: stringOrNull(attrs.author),
|
|
29
|
+
initials: stringOrNull(attrs.initials),
|
|
30
|
+
date: stringOrNull(attrs.date),
|
|
31
|
+
text: stringOrNull(attrs.text) ?? ""
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function replyBodies(value) {
|
|
35
|
+
const replies = /* @__PURE__ */ new Map();
|
|
36
|
+
if (!Array.isArray(value)) return replies;
|
|
37
|
+
for (const entry of value) {
|
|
38
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
39
|
+
const reply = entry;
|
|
40
|
+
const id = stringOrNull(reply.id);
|
|
41
|
+
if (id === null) continue;
|
|
42
|
+
replies.set(id, bodyOf(reply));
|
|
43
|
+
}
|
|
44
|
+
return replies;
|
|
45
|
+
}
|
|
46
|
+
function threadsIn(doc) {
|
|
47
|
+
const threads = /* @__PURE__ */ new Map();
|
|
48
|
+
doc.descendants((node) => {
|
|
49
|
+
if (node.type.name !== "commentReference") return true;
|
|
50
|
+
const id = stringOrNull(node.attrs.id);
|
|
51
|
+
if (id === null || threads.has(id)) return true;
|
|
52
|
+
threads.set(id, {
|
|
53
|
+
...bodyOf(node.attrs),
|
|
54
|
+
replies: replyBodies(node.attrs.replies)
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
return threads;
|
|
59
|
+
}
|
|
60
|
+
function commentOwned(rules, authorId) {
|
|
61
|
+
if (rules.editableComments === "all") return true;
|
|
62
|
+
return authorId === null || authorId === rules.authorId;
|
|
63
|
+
}
|
|
64
|
+
function commentAnchors(doc) {
|
|
65
|
+
const markers = /* @__PURE__ */ new Map();
|
|
66
|
+
const visit = (node, start) => {
|
|
67
|
+
if (isCommentNode(node)) {
|
|
68
|
+
const id = stringOrNull(node.attrs.id);
|
|
69
|
+
if (id !== null) {
|
|
70
|
+
const at = markers.get(id) ?? [];
|
|
71
|
+
at.push(`${node.type.name}@${start}`);
|
|
72
|
+
markers.set(id, at);
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (node.isLeaf) return node.nodeSize;
|
|
77
|
+
let content = 0;
|
|
78
|
+
node.forEach((child) => {
|
|
79
|
+
content += visit(child, start + 1 + content);
|
|
80
|
+
});
|
|
81
|
+
return content + 2;
|
|
82
|
+
};
|
|
83
|
+
visit(doc, -1);
|
|
84
|
+
return new Map(
|
|
85
|
+
Array.from(markers, ([id, at]) => [id, at.join(" ")])
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function sameIdentity(was, now) {
|
|
89
|
+
return was.authorId === now.authorId && was.author === now.author;
|
|
90
|
+
}
|
|
91
|
+
function sameWords(was, now) {
|
|
92
|
+
return was.text === now.text && was.initials === now.initials && was.date === now.date;
|
|
93
|
+
}
|
|
94
|
+
function commentIdentitiesKept(before, after) {
|
|
95
|
+
const now = threadsIn(after);
|
|
96
|
+
for (const [id, thread] of threadsIn(before)) {
|
|
97
|
+
const current = now.get(id);
|
|
98
|
+
if (current === void 0) continue;
|
|
99
|
+
if (!sameIdentity(thread, current)) return false;
|
|
100
|
+
for (const [replyId, reply] of thread.replies) {
|
|
101
|
+
const currentReply = current.replies.get(replyId);
|
|
102
|
+
if (currentReply !== void 0 && !sameIdentity(reply, currentReply)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
function commentEditsOwned(before, after, rules) {
|
|
110
|
+
if (!commentIdentitiesKept(before, after)) return false;
|
|
111
|
+
if (rules.editableComments === "all") return true;
|
|
112
|
+
const now = threadsIn(after);
|
|
113
|
+
const anchoredBefore = commentAnchors(before);
|
|
114
|
+
const anchoredAfter = commentAnchors(after);
|
|
115
|
+
for (const [id, thread] of threadsIn(before)) {
|
|
116
|
+
const current = now.get(id);
|
|
117
|
+
const changed = current === void 0 || !sameWords(thread, current) || anchoredBefore.get(id) !== anchoredAfter.get(id);
|
|
118
|
+
if (changed && !commentOwned(rules, thread.authorId)) return false;
|
|
119
|
+
if (current === void 0) continue;
|
|
120
|
+
for (const [replyId, reply] of thread.replies) {
|
|
121
|
+
const currentReply = current.replies.get(replyId);
|
|
122
|
+
const replyChanged = currentReply === void 0 || !sameWords(reply, currentReply);
|
|
123
|
+
if (replyChanged && !commentOwned(rules, reply.authorId)) return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function protectionAllows(before, after, rules) {
|
|
129
|
+
switch (rules.protection) {
|
|
130
|
+
case "readOnly":
|
|
131
|
+
return false;
|
|
132
|
+
case "comments":
|
|
133
|
+
return changesOnlyComments(before, after) && commentEditsOwned(before, after, rules);
|
|
134
|
+
case "none":
|
|
135
|
+
return !changesOnlyComments(before, after) || commentEditsOwned(before, after, rules);
|
|
136
|
+
default: {
|
|
137
|
+
const unmodelled = rules.protection;
|
|
138
|
+
return unmodelled;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function commentAdditionsBy(before, after, authorId) {
|
|
143
|
+
if (!commentIdentitiesKept(before, after)) return false;
|
|
144
|
+
const was = threadsIn(before);
|
|
145
|
+
for (const [id, thread] of threadsIn(after)) {
|
|
146
|
+
const earlier = was.get(id);
|
|
147
|
+
if (earlier === void 0) {
|
|
148
|
+
if (thread.authorId !== authorId) return false;
|
|
149
|
+
for (const reply of thread.replies.values()) {
|
|
150
|
+
if (reply.authorId !== authorId) return false;
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
for (const [replyId, reply] of thread.replies) {
|
|
155
|
+
if (!earlier.replies.has(replyId) && reply.authorId !== authorId) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
changesOnlyComments,
|
|
164
|
+
commentAdditionsBy,
|
|
165
|
+
commentEditsOwned,
|
|
166
|
+
commentIdentitiesKept,
|
|
167
|
+
commentOwned,
|
|
168
|
+
isCommentNode,
|
|
169
|
+
protectionAllows
|
|
170
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the protection (`./protection`) stands in an editor state, and the questions commands ask
|
|
3
|
+
* of it. Kept apart from the judgements so that those stay free of editor state for the `./core`
|
|
4
|
+
* entry.
|
|
5
|
+
*/
|
|
6
|
+
import { type EditorState, PluginKey } from "prosemirror-state";
|
|
7
|
+
import type { EditingProtection, ProtectionState } from "./protection";
|
|
8
|
+
/** What a state built without the protection plugin runs under, which is what every consumer had before it existed */
|
|
9
|
+
export declare const UNPROTECTED: ProtectionState;
|
|
10
|
+
export declare const protectionKey: PluginKey<ProtectionState>;
|
|
11
|
+
export declare function protectionOf(state: EditorState): ProtectionState;
|
|
12
|
+
export declare function editingProtection(state: EditorState): EditingProtection;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the protection shuts every edit to the body, comments aside.
|
|
15
|
+
*
|
|
16
|
+
* This is the question a command that changes text, formatting or structure asks before it reports
|
|
17
|
+
* that it applies, the way it asks the lock predicates (`./locks`): a command reporting true and
|
|
18
|
+
* then being refused by the guard draws a live control that swallows the click.
|
|
19
|
+
*/
|
|
20
|
+
export declare function editsShut(state: EditorState): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/schema/protectionState.ts
|
|
2
|
+
import { PluginKey } from "prosemirror-state";
|
|
3
|
+
var UNPROTECTED = {
|
|
4
|
+
protection: "none",
|
|
5
|
+
authorId: null,
|
|
6
|
+
editableComments: "own"
|
|
7
|
+
};
|
|
8
|
+
var protectionKey = new PluginKey(
|
|
9
|
+
"docxEditorProtection"
|
|
10
|
+
);
|
|
11
|
+
function protectionOf(state) {
|
|
12
|
+
return protectionKey.getState(state) ?? UNPROTECTED;
|
|
13
|
+
}
|
|
14
|
+
function editingProtection(state) {
|
|
15
|
+
return protectionOf(state).protection;
|
|
16
|
+
}
|
|
17
|
+
function editsShut(state) {
|
|
18
|
+
const protection = editingProtection(state);
|
|
19
|
+
switch (protection) {
|
|
20
|
+
case "none":
|
|
21
|
+
return false;
|
|
22
|
+
case "readOnly":
|
|
23
|
+
case "comments":
|
|
24
|
+
return true;
|
|
25
|
+
default: {
|
|
26
|
+
const unmodelled = protection;
|
|
27
|
+
return unmodelled;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
UNPROTECTED,
|
|
33
|
+
editingProtection,
|
|
34
|
+
editsShut,
|
|
35
|
+
protectionKey,
|
|
36
|
+
protectionOf
|
|
37
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
toCellFormat
|
|
10
10
|
} from "../model/format.js";
|
|
11
11
|
import { isLockedCell, transactionAllowed } from "../schema/locks.js";
|
|
12
|
+
import { editsShut } from "../schema/protectionState.js";
|
|
12
13
|
import { cellDefaultsAt, tableCellSources } from "./gridBorders.js";
|
|
13
14
|
function text(value) {
|
|
14
15
|
return typeof value === "string" ? value : null;
|
|
@@ -59,7 +60,7 @@ function cellFormatCommand(edit) {
|
|
|
59
60
|
const changes = planChanges(rect, edit);
|
|
60
61
|
if (changes.length === 0) return false;
|
|
61
62
|
const transaction = applyChanges(state.tr, rect.tableStart, changes);
|
|
62
|
-
if (!transactionAllowed(transaction, state
|
|
63
|
+
if (!transactionAllowed(transaction, state)) return false;
|
|
63
64
|
dispatch?.(transaction);
|
|
64
65
|
return true;
|
|
65
66
|
};
|
|
@@ -118,6 +119,7 @@ function cellFormats(state) {
|
|
|
118
119
|
return selectedCells(state).map((cell) => toCellFormat(cell.attrs.format));
|
|
119
120
|
}
|
|
120
121
|
function canSetCellFormatting(state) {
|
|
122
|
+
if (editsShut(state)) return false;
|
|
121
123
|
const cells = selectedCells(state);
|
|
122
124
|
return cells.length > 0 && cells.every((cell) => !isLockedCell(cell));
|
|
123
125
|
}
|
package/dist/table/commands.js
CHANGED
|
@@ -21,7 +21,7 @@ function isInTable(state) {
|
|
|
21
21
|
function toCommand(build) {
|
|
22
22
|
return (state, dispatch) => {
|
|
23
23
|
const tr = build(state);
|
|
24
|
-
if (!tr || !transactionAllowed(tr, state
|
|
24
|
+
if (!tr || !transactionAllowed(tr, state)) return false;
|
|
25
25
|
dispatch?.(tr);
|
|
26
26
|
return true;
|
|
27
27
|
};
|
package/dist/table/merge.js
CHANGED
|
@@ -26,11 +26,11 @@ function cellAt(rect, row, col) {
|
|
|
26
26
|
}
|
|
27
27
|
function canMergeCells(state) {
|
|
28
28
|
const tr = buildMergeCellsTransaction(state);
|
|
29
|
-
return tr !== null && transactionAllowed(tr, state
|
|
29
|
+
return tr !== null && transactionAllowed(tr, state);
|
|
30
30
|
}
|
|
31
31
|
function canSplitCell(state) {
|
|
32
32
|
const tr = buildSplitCellTransaction(state);
|
|
33
|
-
return tr !== null && transactionAllowed(tr, state
|
|
33
|
+
return tr !== null && transactionAllowed(tr, state);
|
|
34
34
|
}
|
|
35
35
|
function coveredWidthSum(rect, type) {
|
|
36
36
|
let sum = 0;
|
|
@@ -5,11 +5,11 @@ import { type CommentAuthor } from "../editor/commands/commentCommands";
|
|
|
5
5
|
export interface CommentsPanelProps {
|
|
6
6
|
view: EditorView;
|
|
7
7
|
state: EditorState;
|
|
8
|
-
readOnly: boolean;
|
|
9
8
|
composerOpen: boolean;
|
|
10
|
-
|
|
9
|
+
/** Whose comments the composers write. Null for a reader, who is offered none */
|
|
10
|
+
author: CommentAuthor | null;
|
|
11
11
|
closeComposer: () => void;
|
|
12
12
|
scrollContainer: HTMLElement | null;
|
|
13
13
|
allCommentsOpen: boolean;
|
|
14
14
|
}
|
|
15
|
-
export declare function CommentsPanel({ view, state,
|
|
15
|
+
export declare function CommentsPanel({ view, state, composerOpen, author, closeComposer, scrollContainer, allCommentsOpen, }: CommentsPanelProps): ReactElement;
|
package/dist/ui/CommentsPanel.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
updateComment,
|
|
13
13
|
updateCommentReply
|
|
14
14
|
} from "../editor/commands/commentCommands.js";
|
|
15
|
+
import { commentOwned } from "../schema/protection.js";
|
|
16
|
+
import { protectionOf } from "../schema/protectionState.js";
|
|
15
17
|
import { editorClassNames } from "../styles/classNames.js";
|
|
16
18
|
import { CommentComposer, FocusedTextarea } from "./comments/CommentComposer.js";
|
|
17
19
|
import {
|
|
@@ -41,7 +43,6 @@ function sameTarget(left, right) {
|
|
|
41
43
|
function CommentsPanel({
|
|
42
44
|
view,
|
|
43
45
|
state,
|
|
44
|
-
readOnly,
|
|
45
46
|
composerOpen,
|
|
46
47
|
author,
|
|
47
48
|
closeComposer,
|
|
@@ -49,6 +50,9 @@ function CommentsPanel({
|
|
|
49
50
|
allCommentsOpen
|
|
50
51
|
}) {
|
|
51
52
|
const comments = useMemo(() => documentComments(state), [state.doc]);
|
|
53
|
+
const rules = protectionOf(state);
|
|
54
|
+
const writer = author != null && rules.protection !== "readOnly" ? author : null;
|
|
55
|
+
const owned = (authorId) => writer !== null && commentOwned(rules, authorId);
|
|
52
56
|
const panel = useRef(null);
|
|
53
57
|
const [editing, setEditing] = useState(null);
|
|
54
58
|
const [draft, setDraft] = useState("");
|
|
@@ -108,7 +112,7 @@ function CommentsPanel({
|
|
|
108
112
|
className: editorClassNames.commentsCanvas,
|
|
109
113
|
style: { height: allCommentsOpen ? void 0 : `${canvasHeight}px` },
|
|
110
114
|
children: [
|
|
111
|
-
composerOpen &&
|
|
115
|
+
composerOpen && writer !== null && /* @__PURE__ */ jsx(
|
|
112
116
|
"div",
|
|
113
117
|
{
|
|
114
118
|
className: editorClassNames.commentPosition,
|
|
@@ -119,7 +123,7 @@ function CommentsPanel({
|
|
|
119
123
|
children: /* @__PURE__ */ jsx(
|
|
120
124
|
CommentComposer,
|
|
121
125
|
{
|
|
122
|
-
author,
|
|
126
|
+
author: writer,
|
|
123
127
|
label: "Comment text",
|
|
124
128
|
submitLabel: "Comment",
|
|
125
129
|
onClose: closeComposer,
|
|
@@ -127,8 +131,9 @@ function CommentsPanel({
|
|
|
127
131
|
view,
|
|
128
132
|
addComment({
|
|
129
133
|
text,
|
|
130
|
-
author:
|
|
131
|
-
|
|
134
|
+
author: writer.name,
|
|
135
|
+
authorId: writer.id,
|
|
136
|
+
initials: writer.initials
|
|
132
137
|
})
|
|
133
138
|
)
|
|
134
139
|
}
|
|
@@ -137,7 +142,8 @@ function CommentsPanel({
|
|
|
137
142
|
),
|
|
138
143
|
visible.map((comment) => {
|
|
139
144
|
const rootTarget = { commentId: comment.id, replyId: null };
|
|
140
|
-
const
|
|
145
|
+
const rootOwned = owned(comment.authorId);
|
|
146
|
+
const rootEditing = rootOwned && sameTarget(editing, rootTarget);
|
|
141
147
|
return /* @__PURE__ */ jsx(
|
|
142
148
|
"article",
|
|
143
149
|
{
|
|
@@ -160,8 +166,8 @@ function CommentsPanel({
|
|
|
160
166
|
]
|
|
161
167
|
}
|
|
162
168
|
),
|
|
163
|
-
|
|
164
|
-
!comment.resolved && /* @__PURE__ */ jsx(
|
|
169
|
+
writer !== null && !rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentIconActions, children: [
|
|
170
|
+
rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
|
|
165
171
|
"button",
|
|
166
172
|
{
|
|
167
173
|
type: "button",
|
|
@@ -190,7 +196,7 @@ function CommentsPanel({
|
|
|
190
196
|
children: comment.resolved ? /* @__PURE__ */ jsx(RotateCcw, { size: 16, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": "true" })
|
|
191
197
|
}
|
|
192
198
|
),
|
|
193
|
-
!comment.resolved && /* @__PURE__ */ jsx(
|
|
199
|
+
rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
|
|
194
200
|
"button",
|
|
195
201
|
{
|
|
196
202
|
type: "button",
|
|
@@ -209,7 +215,7 @@ function CommentsPanel({
|
|
|
209
215
|
value: draft,
|
|
210
216
|
onChange: setDraft
|
|
211
217
|
}
|
|
212
|
-
) :
|
|
218
|
+
) : writer === null || comment.resolved ? /* @__PURE__ */ jsx("p", { className: editorClassNames.commentBody, children: comment.text }) : /* @__PURE__ */ jsx(
|
|
213
219
|
"button",
|
|
214
220
|
{
|
|
215
221
|
type: "button",
|
|
@@ -222,7 +228,7 @@ function CommentsPanel({
|
|
|
222
228
|
children: comment.text
|
|
223
229
|
}
|
|
224
230
|
),
|
|
225
|
-
|
|
231
|
+
rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentActions, children: [
|
|
226
232
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setEditing(null), children: "Cancel" }),
|
|
227
233
|
/* @__PURE__ */ jsx(
|
|
228
234
|
"button",
|
|
@@ -234,13 +240,14 @@ function CommentsPanel({
|
|
|
234
240
|
children: "Save"
|
|
235
241
|
}
|
|
236
242
|
)
|
|
237
|
-
] })
|
|
243
|
+
] }),
|
|
238
244
|
comment.replies.map((reply) => {
|
|
239
245
|
const target = {
|
|
240
246
|
commentId: comment.id,
|
|
241
247
|
replyId: reply.id
|
|
242
248
|
};
|
|
243
|
-
const
|
|
249
|
+
const replyOwned = owned(reply.authorId);
|
|
250
|
+
const isEditing = replyOwned && sameTarget(editing, target);
|
|
244
251
|
return /* @__PURE__ */ jsxs(
|
|
245
252
|
"div",
|
|
246
253
|
{
|
|
@@ -258,7 +265,7 @@ function CommentsPanel({
|
|
|
258
265
|
onChange: setDraft
|
|
259
266
|
}
|
|
260
267
|
) : /* @__PURE__ */ jsx("p", { className: editorClassNames.commentBody, children: reply.text }),
|
|
261
|
-
|
|
268
|
+
replyOwned && !comment.resolved && /* @__PURE__ */ jsx("div", { className: editorClassNames.commentActions, children: isEditing ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
262
269
|
/* @__PURE__ */ jsx(
|
|
263
270
|
"button",
|
|
264
271
|
{
|
|
@@ -313,10 +320,10 @@ function CommentsPanel({
|
|
|
313
320
|
reply.id
|
|
314
321
|
);
|
|
315
322
|
}),
|
|
316
|
-
replying === comment.id &&
|
|
323
|
+
replying === comment.id && writer !== null && !comment.resolved && /* @__PURE__ */ jsx(
|
|
317
324
|
CommentComposer,
|
|
318
325
|
{
|
|
319
|
-
author,
|
|
326
|
+
author: writer,
|
|
320
327
|
label: "Reply text",
|
|
321
328
|
submitLabel: "Reply",
|
|
322
329
|
onClose: () => setReplying(null),
|
|
@@ -324,8 +331,9 @@ function CommentsPanel({
|
|
|
324
331
|
view,
|
|
325
332
|
addCommentReply(comment.id, {
|
|
326
333
|
text,
|
|
327
|
-
author:
|
|
328
|
-
|
|
334
|
+
author: writer.name,
|
|
335
|
+
authorId: writer.id,
|
|
336
|
+
initials: writer.initials
|
|
329
337
|
})
|
|
330
338
|
)
|
|
331
339
|
}
|