@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.
- package/CHANGELOG.md +29 -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/formatting/reading.js +13 -4
- 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/numbering/markers.js +13 -1
- 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/styles/fontStack.d.ts +9 -0
- package/dist/styles/fontStack.js +9 -8
- 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/FontFamilySelect.js +21 -7
- 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/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type { CellFormat, CellVerticalAlign, DocumentDefaults, HighlightName, Li
|
|
|
22
22
|
export type { DocxExportErrorCode, DocxImportErrorCode, } from "./ooxml/errors";
|
|
23
23
|
export { DocxExportError, DocxImportError } from "./ooxml/errors";
|
|
24
24
|
export { docxSchema } from "./schema";
|
|
25
|
+
/** Whose comments the panel offers to edit, which `DocxEditorMode` takes */
|
|
26
|
+
export type { EditableComments } from "./schema/protection";
|
|
25
27
|
/**
|
|
26
28
|
* The lists the built-in pickers offer. They are exported so that a toolbar of your own can
|
|
27
29
|
* offer the same ones without writing them out again; the pickers themselves read these very
|
|
@@ -70,6 +70,18 @@ function advance(counters, list, ilvl, start) {
|
|
|
70
70
|
if (deeper > ilvl) counters.delete(deeper);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
var MARKER_SEGMENTER = new Intl.Segmenter();
|
|
74
|
+
function capped(shape) {
|
|
75
|
+
if (shape.length <= MAX_MARKER_CHARS) return shape;
|
|
76
|
+
let text = "";
|
|
77
|
+
for (const { segment } of MARKER_SEGMENTER.segment(
|
|
78
|
+
shape.slice(0, MAX_MARKER_CHARS * 2)
|
|
79
|
+
)) {
|
|
80
|
+
if (text.length + segment.length > MAX_MARKER_CHARS) break;
|
|
81
|
+
text += segment;
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
73
85
|
function computeMarkers(paragraphs, numbering) {
|
|
74
86
|
const countersByList = /* @__PURE__ */ new Map();
|
|
75
87
|
return paragraphs.map((ref) => {
|
|
@@ -84,7 +96,7 @@ function computeMarkers(paragraphs, numbering) {
|
|
|
84
96
|
}
|
|
85
97
|
advance(counters, list, ref.ilvl, level.start);
|
|
86
98
|
const shape = level.format === "bullet" ? level.text : fillLevels(level.text, list, counters);
|
|
87
|
-
const text = shape
|
|
99
|
+
const text = capped(shape);
|
|
88
100
|
return text ? { text, indent: levelIndentPt(level.indent) } : null;
|
|
89
101
|
});
|
|
90
102
|
}
|
|
@@ -556,6 +556,9 @@ var docxSchema = new Schema({
|
|
|
556
556
|
id: { default: null },
|
|
557
557
|
referenceXml: { default: null },
|
|
558
558
|
author: { default: null },
|
|
559
|
+
// The identity behind the display name, read from and written to the people part. Null for
|
|
560
|
+
// a comment nobody's identity is recorded for, which every comment made in Word is here
|
|
561
|
+
authorId: { default: null },
|
|
559
562
|
initials: { default: null },
|
|
560
563
|
date: { default: null },
|
|
561
564
|
text: { default: "" },
|
|
@@ -576,6 +579,7 @@ var docxSchema = new Schema({
|
|
|
576
579
|
"data-comment-id": text(node.attrs.id),
|
|
577
580
|
"data-reference-xml": text(node.attrs.referenceXml),
|
|
578
581
|
"data-comment-author": text(node.attrs.author),
|
|
582
|
+
"data-comment-author-id": text(node.attrs.authorId),
|
|
579
583
|
"data-comment-initials": text(node.attrs.initials),
|
|
580
584
|
"data-comment-date": text(node.attrs.date),
|
|
581
585
|
"data-comment-text": text(node.attrs.text),
|
|
@@ -596,6 +600,7 @@ var docxSchema = new Schema({
|
|
|
596
600
|
id: dom.getAttribute("data-comment-id"),
|
|
597
601
|
referenceXml: dom.getAttribute("data-reference-xml"),
|
|
598
602
|
author: dom.getAttribute("data-comment-author"),
|
|
603
|
+
authorId: dom.getAttribute("data-comment-author-id"),
|
|
599
604
|
initials: dom.getAttribute("data-comment-initials"),
|
|
600
605
|
date: dom.getAttribute("data-comment-date"),
|
|
601
606
|
text: dom.getAttribute("data-comment-text") ?? "",
|
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
|
+
};
|
|
@@ -36,6 +36,15 @@ export interface FontFallbacks {
|
|
|
36
36
|
* own set (`fontFallbacks` on `DocxEditor`).
|
|
37
37
|
*/
|
|
38
38
|
export declare const DEFAULT_FONT_FALLBACKS: FontFallbacks;
|
|
39
|
+
/**
|
|
40
|
+
* The form a name is compared in. Quotes, letter case, and Unicode normalization all vary
|
|
41
|
+
* from document to document, so two spellings of one name have to compare equal.
|
|
42
|
+
*
|
|
43
|
+
* NFC only. An NFK* form is lossy and would fold the fullwidth Latin of a name such as
|
|
44
|
+
* `DFKaiShu` into ASCII. The result is a comparison key and is never
|
|
45
|
+
* written into a document or shown: the original spelling is what gets stored and exported.
|
|
46
|
+
*/
|
|
47
|
+
export declare function comparableFontName(name: string): string;
|
|
39
48
|
/**
|
|
40
49
|
* Whether this font name belongs to a script written in Han characters, kana or
|
|
41
50
|
* Hangul.
|
package/dist/styles/fontStack.js
CHANGED
|
@@ -189,15 +189,15 @@ var DEFAULT_FONT_FALLBACKS = {
|
|
|
189
189
|
defaultStack: "Arial, Helvetica, sans-serif",
|
|
190
190
|
defaultFontName: "Arial"
|
|
191
191
|
};
|
|
192
|
-
function
|
|
193
|
-
return name.trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
192
|
+
function comparableFontName(name) {
|
|
193
|
+
return name.trim().replace(/^["']|["']$/g, "").toLowerCase().normalize("NFC");
|
|
194
194
|
}
|
|
195
195
|
var EAST_ASIAN_CHARACTER = /[\u1100-\u11ff\u3000-\u30ff\u3130-\u318f\u31f0-\u31ff\u3400-\u4dbf\u4e00-\u9fff\ua960-\ua97f\uac00-\ud7ff\uf900-\ufaff\uff00-\uffef\u{20000}-\u{2fa1f}]/u;
|
|
196
196
|
var eastAsianNames = new Set(
|
|
197
|
-
EAST_ASIAN_GROUPS.flatMap((group) => group.names.map(
|
|
197
|
+
EAST_ASIAN_GROUPS.flatMap((group) => group.names.map(comparableFontName))
|
|
198
198
|
);
|
|
199
199
|
function isEastAsianFontName(name) {
|
|
200
|
-
return EAST_ASIAN_CHARACTER.test(name) || eastAsianNames.has(
|
|
200
|
+
return EAST_ASIAN_CHARACTER.test(name) || eastAsianNames.has(comparableFontName(name));
|
|
201
201
|
}
|
|
202
202
|
var stacksByFallbacks = /* @__PURE__ */ new WeakMap();
|
|
203
203
|
function stackByName(fallbacks) {
|
|
@@ -206,7 +206,7 @@ function stackByName(fallbacks) {
|
|
|
206
206
|
const stacks = /* @__PURE__ */ new Map();
|
|
207
207
|
for (const group of fallbacks.groups) {
|
|
208
208
|
for (const name of group.names)
|
|
209
|
-
stacks.set(
|
|
209
|
+
stacks.set(comparableFontName(name), group.stack);
|
|
210
210
|
}
|
|
211
211
|
stacksByFallbacks.set(fallbacks, stacks);
|
|
212
212
|
return stacks;
|
|
@@ -215,7 +215,7 @@ function matchedStacks(cssNames, fallbacks) {
|
|
|
215
215
|
const stacks = stackByName(fallbacks);
|
|
216
216
|
const matched = [];
|
|
217
217
|
for (const name of cssNames.split(",")) {
|
|
218
|
-
const stack = stacks.get(
|
|
218
|
+
const stack = stacks.get(comparableFontName(name));
|
|
219
219
|
if (stack !== void 0 && !matched.includes(stack)) matched.push(stack);
|
|
220
220
|
}
|
|
221
221
|
return matched;
|
|
@@ -235,12 +235,12 @@ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
|
235
235
|
"emoji"
|
|
236
236
|
]);
|
|
237
237
|
function isGenericFamily(name) {
|
|
238
|
-
return GENERIC_FAMILIES.has(
|
|
238
|
+
return GENERIC_FAMILIES.has(comparableFontName(name));
|
|
239
239
|
}
|
|
240
240
|
function distinctNames(names) {
|
|
241
241
|
const seen = /* @__PURE__ */ new Set();
|
|
242
242
|
return names.filter((name) => {
|
|
243
|
-
const comparable =
|
|
243
|
+
const comparable = comparableFontName(name);
|
|
244
244
|
if (seen.has(comparable)) return false;
|
|
245
245
|
seen.add(comparable);
|
|
246
246
|
return true;
|
|
@@ -257,6 +257,7 @@ function withFontFallback(cssNames, fallbacks = DEFAULT_FONT_FALLBACKS) {
|
|
|
257
257
|
}
|
|
258
258
|
export {
|
|
259
259
|
DEFAULT_FONT_FALLBACKS,
|
|
260
|
+
comparableFontName,
|
|
260
261
|
isEastAsianFontName,
|
|
261
262
|
withFontFallback
|
|
262
263
|
};
|
|
@@ -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;
|