@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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// src/editor/commands/comments/reading.ts
|
|
2
|
+
import { commentOwned } from "../../../schema/protection.js";
|
|
3
|
+
import { protectionOf } from "../../../schema/protectionState.js";
|
|
2
4
|
function stringAttr(value) {
|
|
3
5
|
return typeof value === "string" ? value : null;
|
|
4
6
|
}
|
|
@@ -34,6 +36,7 @@ function documentComments(state) {
|
|
|
34
36
|
return {
|
|
35
37
|
id,
|
|
36
38
|
author: stringAttr(node.attrs.author),
|
|
39
|
+
authorId: stringAttr(node.attrs.authorId),
|
|
37
40
|
initials: stringAttr(node.attrs.initials),
|
|
38
41
|
date: stringAttr(node.attrs.date),
|
|
39
42
|
text: stringAttr(node.attrs.text) ?? "",
|
|
@@ -44,6 +47,7 @@ function documentComments(state) {
|
|
|
44
47
|
replies: repliesAttr(node.attrs.replies).map((reply) => ({
|
|
45
48
|
id: reply.id,
|
|
46
49
|
author: reply.author,
|
|
50
|
+
authorId: reply.authorId,
|
|
47
51
|
initials: reply.initials,
|
|
48
52
|
date: reply.date,
|
|
49
53
|
text: reply.text
|
|
@@ -51,7 +55,17 @@ function documentComments(state) {
|
|
|
51
55
|
};
|
|
52
56
|
});
|
|
53
57
|
}
|
|
58
|
+
function canEditComment(state, commentId, replyId = null) {
|
|
59
|
+
const rules = protectionOf(state);
|
|
60
|
+
if (rules.protection === "readOnly") return false;
|
|
61
|
+
const comment = documentComments(state).find(
|
|
62
|
+
(entry) => entry.id === commentId
|
|
63
|
+
);
|
|
64
|
+
const body = replyId === null ? comment : comment?.replies.find((reply) => reply.id === replyId);
|
|
65
|
+
return body !== void 0 && commentOwned(rules, body.authorId);
|
|
66
|
+
}
|
|
54
67
|
export {
|
|
68
|
+
canEditComment,
|
|
55
69
|
documentComments,
|
|
56
70
|
repliesAttr,
|
|
57
71
|
stringAttr
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
insertionInsideLocked,
|
|
10
10
|
rangeTouchesLocked
|
|
11
11
|
} from "../../../schema/locks.js";
|
|
12
|
+
import { editsShut } from "../../../schema/protectionState.js";
|
|
12
13
|
import {
|
|
13
14
|
activePieces,
|
|
14
15
|
caretPiece,
|
|
@@ -69,9 +70,13 @@ function applyToCaret(state, dispatch, edit) {
|
|
|
69
70
|
return true;
|
|
70
71
|
}
|
|
71
72
|
function runEditCommand(edit) {
|
|
72
|
-
return (state, dispatch) =>
|
|
73
|
+
return (state, dispatch) => {
|
|
74
|
+
if (editsShut(state)) return false;
|
|
75
|
+
return state.selection.empty ? applyToCaret(state, dispatch, edit) : applyToSelection(state, dispatch, edit);
|
|
76
|
+
};
|
|
73
77
|
}
|
|
74
78
|
function canFormatText(state) {
|
|
79
|
+
if (editsShut(state)) return false;
|
|
75
80
|
if (state.selection.empty) {
|
|
76
81
|
return !insertionInsideLocked(state.doc, state.selection.from);
|
|
77
82
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
toRunFormat
|
|
6
6
|
} from "../../../model/format.js";
|
|
7
7
|
import {
|
|
8
|
+
comparableFontName,
|
|
8
9
|
DEFAULT_FONT_FALLBACKS
|
|
9
10
|
} from "../../../styles/fontStack.js";
|
|
10
11
|
import {
|
|
@@ -40,22 +41,30 @@ function defaultFontName(state, fontFallbacks) {
|
|
|
40
41
|
function activeFontFamily(state, fontFallbacks = DEFAULT_FONT_FALLBACKS) {
|
|
41
42
|
const names = activePieces(state).map((target) => fontNameOf(target.format));
|
|
42
43
|
const first = names[0] ?? null;
|
|
43
|
-
|
|
44
|
+
const keys = names.map(
|
|
45
|
+
(name) => name === null ? null : comparableFontName(name)
|
|
46
|
+
);
|
|
47
|
+
if (keys.some((key) => key !== keys[0])) return { kind: "mixed" };
|
|
44
48
|
if (first !== null) return { kind: "font", name: first };
|
|
45
49
|
return { kind: "default", name: defaultFontName(state, fontFallbacks) };
|
|
46
50
|
}
|
|
47
51
|
function documentFontNames(doc, defaults) {
|
|
48
|
-
const names = new
|
|
52
|
+
const names = /* @__PURE__ */ new Map();
|
|
53
|
+
const collect = (name) => {
|
|
54
|
+
const comparable = comparableFontName(name);
|
|
55
|
+
if (!names.has(comparable)) names.set(comparable, name);
|
|
56
|
+
};
|
|
57
|
+
for (const name of fontNamesOf(defaults.fontFamily)) collect(name);
|
|
49
58
|
doc.descendants((node) => {
|
|
50
59
|
const mark = runMarkOf(node.marks);
|
|
51
60
|
for (const name of fontNamesOf(
|
|
52
61
|
toRunFormat(mark?.attrs.format)?.fontFamily
|
|
53
62
|
)) {
|
|
54
|
-
|
|
63
|
+
collect(name);
|
|
55
64
|
}
|
|
56
65
|
return true;
|
|
57
66
|
});
|
|
58
|
-
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
67
|
+
return Array.from(names.values()).sort((a, b) => a.localeCompare(b));
|
|
59
68
|
}
|
|
60
69
|
function activeTextColor(state) {
|
|
61
70
|
return sharedValue(
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// src/editor/commands/historyCommands.ts
|
|
2
2
|
import { redo as historyRedo, undo as historyUndo } from "prosemirror-history";
|
|
3
|
-
import { historyReplay } from "../../schema/locks.js";
|
|
3
|
+
import { historyReplay, transactionAllowed } from "../../schema/locks.js";
|
|
4
4
|
function replaying(command) {
|
|
5
|
-
return (state, dispatch, view) =>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
return (state, dispatch, view) => {
|
|
6
|
+
const built = [];
|
|
7
|
+
const answered = command(
|
|
8
|
+
state,
|
|
9
|
+
(tr) => built.push(tr.setMeta(historyReplay, true)),
|
|
10
|
+
view
|
|
11
|
+
);
|
|
12
|
+
if (!answered || !built.every((tr) => transactionAllowed(tr, state))) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (dispatch) for (const tr of built) dispatch(tr);
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
10
18
|
}
|
|
11
19
|
var undo = replaying(historyUndo);
|
|
12
20
|
var redo = replaying(historyRedo);
|
|
@@ -15,6 +15,12 @@ export type { LineSpacing, ParagraphAlign } from "../../model/format";
|
|
|
15
15
|
export type { ListKind } from "../../numbering/listTemplate";
|
|
16
16
|
/** The size an image is shown at, which the insert command asks for */
|
|
17
17
|
export type { ImageExtent } from "../../ooxml/image";
|
|
18
|
+
/**
|
|
19
|
+
* What the editor as a whole may receive - everything, comments alone, or nothing - which a control
|
|
20
|
+
* of your own reads before it offers an edit, the way the built-in ones do.
|
|
21
|
+
*/
|
|
22
|
+
export type { EditableComments, EditingProtection, } from "../../schema/protection";
|
|
23
|
+
export { editingProtection } from "../../schema/protectionState";
|
|
18
24
|
/** The fonts to draw with where a document names one the machine does not have, which the font queries read */
|
|
19
25
|
export type { FontFallbacks } from "../../styles/fontStack";
|
|
20
26
|
/**
|
|
@@ -49,7 +55,7 @@ export { insertLineBreak, insertPageBreak } from "./breakCommands";
|
|
|
49
55
|
*/
|
|
50
56
|
export { canRunCommand } from "./canRunCommand";
|
|
51
57
|
export type { CommentAuthor, DocumentComment, DocumentCommentReply, NewComment, } from "./commentCommands";
|
|
52
|
-
export { addComment, addCommentReply, canAddComment, documentComments, removeComment, removeCommentReply, selectComment, setCommentResolved, updateComment, updateCommentReply, } from "./commentCommands";
|
|
58
|
+
export { addComment, addCommentReply, canAddComment, canEditComment, documentComments, removeComment, removeCommentReply, selectComment, setCommentResolved, updateComment, updateCommentReply, } from "./commentCommands";
|
|
53
59
|
export type { ActiveFontFamily, ActiveFontSize, } from "./formattingCommands";
|
|
54
60
|
export { activeFontFamily, activeFontSize, activeTextBackground, activeTextColor, canFormatText, documentFontNames, isBoldActive, isItalicActive, isStrikeActive, isUnderlineActive, setFontFamily, setFontSize, setTextBackground, setTextColor, toggleBold, toggleItalic, toggleStrike, toggleUnderline, } from "./formattingCommands";
|
|
55
61
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/editor/commands/index.ts
|
|
2
|
+
import { editingProtection } from "../../schema/protectionState.js";
|
|
2
3
|
import {
|
|
3
4
|
documentBodyWidthPx,
|
|
4
5
|
documentDefaults,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
addComment,
|
|
20
21
|
addCommentReply,
|
|
21
22
|
canAddComment,
|
|
23
|
+
canEditComment,
|
|
22
24
|
documentComments,
|
|
23
25
|
removeComment,
|
|
24
26
|
removeCommentReply,
|
|
@@ -108,6 +110,7 @@ export {
|
|
|
108
110
|
addCommentReply,
|
|
109
111
|
canAddComment,
|
|
110
112
|
canDecreaseIndent,
|
|
113
|
+
canEditComment,
|
|
111
114
|
canFormatText,
|
|
112
115
|
canIncreaseIndent,
|
|
113
116
|
canInsertImage,
|
|
@@ -125,6 +128,7 @@ export {
|
|
|
125
128
|
documentHasLocked,
|
|
126
129
|
documentNotes,
|
|
127
130
|
documentParagraphStyles,
|
|
131
|
+
editingProtection,
|
|
128
132
|
fittedExtent,
|
|
129
133
|
imageFilesIn,
|
|
130
134
|
increaseIndent,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/editor/commands/linkCommands.ts
|
|
2
2
|
import { docxSchema } from "../../schema/index.js";
|
|
3
3
|
import { rangeTouchesLocked } from "../../schema/locks.js";
|
|
4
|
+
import { editsShut } from "../../schema/protectionState.js";
|
|
4
5
|
var linkType = docxSchema.marks.link;
|
|
5
6
|
function text(value) {
|
|
6
7
|
return typeof value === "string" ? value : null;
|
|
@@ -74,6 +75,7 @@ function linkSpansTouching(doc, from, to) {
|
|
|
74
75
|
return spans;
|
|
75
76
|
}
|
|
76
77
|
function openPieces(state) {
|
|
78
|
+
if (editsShut(state)) return [];
|
|
77
79
|
const found = state.selection.empty ? caretPieces(state) : selectionPieces(state);
|
|
78
80
|
return found.filter(
|
|
79
81
|
(piece) => !rangeTouchesLocked(state.doc, piece.from, piece.to)
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
selectionShut,
|
|
14
14
|
unlockAllowed
|
|
15
15
|
} from "../../schema/locks.js";
|
|
16
|
+
import { editsShut } from "../../schema/protectionState.js";
|
|
16
17
|
function lockedControlMark(id) {
|
|
17
18
|
return docxSchema.marks.sdt.create({
|
|
18
19
|
sdtPrefix: `<w:sdt><w:sdtPr><w:id w:val="${id}"/><w:lock w:val="sdtContentLocked"/></w:sdtPr>`,
|
|
@@ -88,6 +89,7 @@ function blockLockEdits(doc, block, spans, reach) {
|
|
|
88
89
|
return [...shut, ...fresh];
|
|
89
90
|
}
|
|
90
91
|
function selectionLockDetail(state) {
|
|
92
|
+
if (editsShut(state)) return { kind: "none" };
|
|
91
93
|
const selection = state.selection;
|
|
92
94
|
const locking = !selection.empty && selection instanceof TextSelection;
|
|
93
95
|
const edits = [];
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import { toParagraphFormat } from "../../model/format.js";
|
|
6
6
|
import { docxSchema } from "../../schema/index.js";
|
|
7
7
|
import { replacementShut } from "../../schema/locks.js";
|
|
8
|
+
import { editsShut } from "../../schema/protectionState.js";
|
|
8
9
|
function isOrdinaryParagraph(state) {
|
|
9
10
|
const { $from } = state.selection;
|
|
10
11
|
if ($from.parent.type !== docxSchema.nodes.paragraph || toParagraphFormat($from.parent.attrs.format)?.numbering) {
|
|
@@ -17,7 +18,7 @@ function isOrdinaryParagraph(state) {
|
|
|
17
18
|
}
|
|
18
19
|
var insertTab = (state, dispatch) => {
|
|
19
20
|
const { selection } = state;
|
|
20
|
-
if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || replacementShut(selection, state.doc)) {
|
|
21
|
+
if (!(selection instanceof TextSelection) || !selection.$from.sameParent(selection.$to) || !isOrdinaryParagraph(state) || editsShut(state) || replacementShut(selection, state.doc)) {
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
if (dispatch) {
|
|
@@ -8,7 +8,9 @@ import { type ParagraphFormatLayer, type ParagraphStyleOption, type StyleTable }
|
|
|
8
8
|
import { type PageGeometry } from "../docx/pageGeometry";
|
|
9
9
|
import type { DocumentDefaults } from "../model/format";
|
|
10
10
|
import { type Numbering } from "../numbering/parseNumbering";
|
|
11
|
+
import type { EditableComments, EditingProtection } from "../schema/protection";
|
|
11
12
|
import { type FontFallbacks } from "../styles/fontStack";
|
|
13
|
+
import type { CommentAuthor } from "./commands/comments/model";
|
|
12
14
|
export interface EditorStateOptions {
|
|
13
15
|
numbering?: Numbering;
|
|
14
16
|
styles?: StyleTable;
|
|
@@ -38,22 +40,30 @@ export interface EditorStateOptions {
|
|
|
38
40
|
reservedCommentIds?: Iterable<string>;
|
|
39
41
|
/** Every paragraph id present in the opened comment parts, including orphan extension entries. */
|
|
40
42
|
reservedCommentParaIds?: Iterable<string>;
|
|
43
|
+
/**
|
|
44
|
+
* What the document as a whole may receive (`schema/protection`): everything, comments alone,
|
|
45
|
+
* or nothing. Everything when none is given, which is what every state was before.
|
|
46
|
+
*/
|
|
47
|
+
protection?: EditingProtection;
|
|
48
|
+
/** Whose comments are written, and so whose may be edited under `editableComments: "own"` */
|
|
49
|
+
author?: CommentAuthor | null;
|
|
50
|
+
/** Whose comments may be edited or deleted. One's own when none is given */
|
|
51
|
+
editableComments?: EditableComments;
|
|
41
52
|
}
|
|
42
53
|
/**
|
|
43
54
|
* Creates a single editing state.
|
|
44
55
|
* The list definitions, the style table, and the document defaults are what the document
|
|
45
56
|
* wrote down, and commands and the toolbar read them through plugins.
|
|
46
57
|
*/
|
|
47
|
-
export declare function createEditorState(doc: PMNode, { numbering, styles, defaults, paragraphDefaults, canStartNewList, consumerPlugins, paragraphStyles, contextMenus, geometry, defaultTabStopPt, reservedCommentIds, reservedCommentParaIds, }?: EditorStateOptions): EditorState;
|
|
58
|
+
export declare function createEditorState(doc: PMNode, { numbering, styles, defaults, paragraphDefaults, canStartNewList, consumerPlugins, paragraphStyles, contextMenus, geometry, defaultTabStopPt, reservedCommentIds, reservedCommentParaIds, protection, author, editableComments, }?: EditorStateOptions): EditorState;
|
|
48
59
|
export interface EditorOptions {
|
|
49
60
|
mount: HTMLElement;
|
|
50
61
|
state: EditorState;
|
|
51
62
|
defaults: DocumentDefaults;
|
|
52
63
|
/** The paper the document names. The sheet is drawn from it, A4 where a document names none */
|
|
53
64
|
geometry?: PageGeometry;
|
|
54
|
-
readOnly: boolean;
|
|
55
65
|
/** The fonts stood in for the ones the document declares. The built-in set when none is given */
|
|
56
66
|
fontFallbacks?: FontFallbacks;
|
|
57
67
|
onStateChange: (state: EditorState) => void;
|
|
58
68
|
}
|
|
59
|
-
export declare function createEditorView({ mount, state, defaults, geometry,
|
|
69
|
+
export declare function createEditorView({ mount, state, defaults, geometry, fontFallbacks, onStateChange, }: EditorOptions): EditorView;
|
|
@@ -15,6 +15,7 @@ import { A4_PORTRAIT } from "../docx/pageGeometry.js";
|
|
|
15
15
|
import { EMPTY_NUMBERING } from "../numbering/parseNumbering.js";
|
|
16
16
|
import { pageDecorations } from "../page/pageDecorations.js";
|
|
17
17
|
import { pageGeometryStyle, pagePixels } from "../page/pageLayout.js";
|
|
18
|
+
import { editsShut } from "../schema/protectionState.js";
|
|
18
19
|
import { editorClassNames } from "../styles/classNames.js";
|
|
19
20
|
import {
|
|
20
21
|
DEFAULT_FONT_FALLBACKS
|
|
@@ -28,8 +29,9 @@ import { bookmarkProtection } from "./plugins/bookmarkProtection.js";
|
|
|
28
29
|
import { columnResize } from "./plugins/columnResize.js";
|
|
29
30
|
import { commentDecorations } from "./plugins/commentDecorations.js";
|
|
30
31
|
import { commentReservations } from "./plugins/commentReservations.js";
|
|
32
|
+
import { documentProtection } from "./plugins/documentProtection.js";
|
|
31
33
|
import { imagePaste } from "./plugins/imagePaste.js";
|
|
32
|
-
import { docxKeymap } from "./plugins/keymap.js";
|
|
34
|
+
import { docxKeymap, historyKeys } from "./plugins/keymap.js";
|
|
33
35
|
import { linkPanel } from "./plugins/linkPanel.js";
|
|
34
36
|
import { listInputRules } from "./plugins/listInputRules.js";
|
|
35
37
|
import { lockedContent } from "./plugins/lockedContent.js";
|
|
@@ -57,7 +59,10 @@ function createEditorState(doc, {
|
|
|
57
59
|
geometry = A4_PORTRAIT,
|
|
58
60
|
defaultTabStopPt = DEFAULT_TAB_STOP_PT,
|
|
59
61
|
reservedCommentIds = [],
|
|
60
|
-
reservedCommentParaIds = []
|
|
62
|
+
reservedCommentParaIds = [],
|
|
63
|
+
protection = "none",
|
|
64
|
+
author = null,
|
|
65
|
+
editableComments = "own"
|
|
61
66
|
} = {}) {
|
|
62
67
|
return EditorState.create({
|
|
63
68
|
doc: withDerivedGridBorders(doc),
|
|
@@ -68,7 +73,9 @@ function createEditorState(doc, {
|
|
|
68
73
|
...consumerPlugins,
|
|
69
74
|
// Refuses every edit inside a locked content control, whoever asked for it. It is not
|
|
70
75
|
// optional: a document that locked a part of itself stays locked in every consumer.
|
|
76
|
+
// The same guard refuses what the protection below shuts.
|
|
71
77
|
lockedContent(),
|
|
78
|
+
documentProtection({ protection, author, editableComments }),
|
|
72
79
|
// Bookmark ranges are preserved rather than edited, so neither half may disappear.
|
|
73
80
|
bookmarkProtection(),
|
|
74
81
|
// Note bodies are display-only, so their imported main-story references stay paired.
|
|
@@ -77,6 +84,7 @@ function createEditorState(doc, {
|
|
|
77
84
|
commentReservations(reservedCommentIds, reservedCommentParaIds),
|
|
78
85
|
history(),
|
|
79
86
|
keymap(docxKeymap),
|
|
87
|
+
historyKeys(),
|
|
80
88
|
keymap(baseKeymap),
|
|
81
89
|
// Holds whether the link panel is open, which Cmd+K above and the toolbar button both set
|
|
82
90
|
linkPanel(),
|
|
@@ -128,18 +136,24 @@ function createEditorView({
|
|
|
128
136
|
state,
|
|
129
137
|
defaults,
|
|
130
138
|
geometry = A4_PORTRAIT,
|
|
131
|
-
readOnly,
|
|
132
139
|
fontFallbacks = DEFAULT_FONT_FALLBACKS,
|
|
133
140
|
onStateChange
|
|
134
141
|
}) {
|
|
142
|
+
const sheetStyle = `${pageGeometryStyle(pagePixels(geometry))};${documentDefaultsStyle(defaults, fontFallbacks)};`;
|
|
135
143
|
const view = new EditorView(mount, {
|
|
136
144
|
state,
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
// A protection that shuts the body shuts typing with it, and is read off the state so that a
|
|
146
|
+
// mode switched on an open document takes effect without a new view. Selecting stays open
|
|
147
|
+
// either way, which is what a reader marking a stretch for a comment needs
|
|
148
|
+
editable: (current) => !editsShut(current),
|
|
149
|
+
attributes: (current) => ({
|
|
139
150
|
class: editorClassNames.sheet,
|
|
140
151
|
// The paper first, so a document that names one is drawn on it from the first frame
|
|
141
|
-
style: `${
|
|
142
|
-
|
|
152
|
+
style: `${sheetStyle}tab-size:${documentDefaultTabStopPt(current)}pt`,
|
|
153
|
+
// A sheet that takes no typing is no longer focusable of itself, so a reader or a commenter
|
|
154
|
+
// is handed the focus another way: the keys reach it, and the selection stays its own
|
|
155
|
+
...editsShut(current) ? { tabindex: "0" } : {}
|
|
156
|
+
}),
|
|
143
157
|
// The schema can only draw a run with the default fallback fonts, so this editor draws its own
|
|
144
158
|
markViews: { run: runMarkView(fontFallbacks) },
|
|
145
159
|
// An image is drawn by a view of its own, which is what carries the resize handles
|
|
@@ -3,8 +3,11 @@ import { insertPoint } from "prosemirror-transform";
|
|
|
3
3
|
import { toImageExtent, toImageSrc } from "../ooxml/image.js";
|
|
4
4
|
import { docxSchema } from "../schema/index.js";
|
|
5
5
|
import { replacementShut } from "../schema/locks.js";
|
|
6
|
+
import { editsShut } from "../schema/protectionState.js";
|
|
6
7
|
function insertPosition(state) {
|
|
7
|
-
if (replacementShut(state.selection, state.doc))
|
|
8
|
+
if (editsShut(state) || replacementShut(state.selection, state.doc)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
8
11
|
return insertPoint(state.doc, state.selection.from, docxSchema.nodes.image);
|
|
9
12
|
}
|
|
10
13
|
function canInsertImage(state) {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
TextSelection
|
|
5
5
|
} from "prosemirror-state";
|
|
6
6
|
import { createTableNode, isTableSide } from "../docx/tableTemplate.js";
|
|
7
|
+
import { editsShut } from "../schema/protectionState.js";
|
|
7
8
|
import { documentGeometry } from "./documentStyles.js";
|
|
8
9
|
function isInTable($pos) {
|
|
9
10
|
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
|
@@ -13,7 +14,7 @@ function isInTable($pos) {
|
|
|
13
14
|
}
|
|
14
15
|
function insertPosition(state) {
|
|
15
16
|
const $from = state.selection.$from;
|
|
16
|
-
if ($from.depth === 0 || isInTable($from)) return null;
|
|
17
|
+
if (editsShut(state) || $from.depth === 0 || isInTable($from)) return null;
|
|
17
18
|
return $from.after(1);
|
|
18
19
|
}
|
|
19
20
|
function canInsertTable(state) {
|
|
@@ -28,6 +28,8 @@ export declare function selectedParagraphs(state: EditorState): ParagraphSpot[];
|
|
|
28
28
|
* Only the paragraphs of a locked cell are shut. A paragraph merely holding a locked control keeps
|
|
29
29
|
* its own alignment, indent and style, which is what `insideLockedCell` says and
|
|
30
30
|
* `rangeTouchesLocked` would not (`schema/locks`).
|
|
31
|
+
*
|
|
32
|
+
* A protection that shuts the body leaves no paragraph editable (`schema/protection`).
|
|
31
33
|
*/
|
|
32
34
|
export declare function editableParagraphs(state: EditorState): ParagraphSpot[];
|
|
33
35
|
/** The original formatting XML this paragraph holds. Null when there is none */
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { effectiveParagraphFormat } from "../docx/formatting.js";
|
|
3
3
|
import { docxSchema } from "../schema/index.js";
|
|
4
4
|
import { insideLockedCell } from "../schema/locks.js";
|
|
5
|
+
import { editsShut } from "../schema/protectionState.js";
|
|
5
6
|
import {
|
|
6
7
|
defaultParagraphStyleId,
|
|
7
8
|
documentParagraphFormatting,
|
|
@@ -19,6 +20,7 @@ function selectedParagraphs(state) {
|
|
|
19
20
|
return Array.from(spots, ([pos, node]) => ({ pos, node }));
|
|
20
21
|
}
|
|
21
22
|
function editableParagraphs(state) {
|
|
23
|
+
if (editsShut(state)) return [];
|
|
22
24
|
return selectedParagraphs(state).filter(
|
|
23
25
|
(spot) => !insideLockedCell(state.doc, spot.pos)
|
|
24
26
|
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the protection the editor runs under (`schema/protection`) in the state, where the guard
|
|
3
|
+
* and the commands read it. The guard itself stands in `lockedContent`, which enforces
|
|
4
|
+
* `transactionAllowed` for locks and protection in one pass.
|
|
5
|
+
*/
|
|
6
|
+
import { Plugin, type Transaction } from "prosemirror-state";
|
|
7
|
+
import type { EditableComments, EditingProtection, ProtectionState } from "../../schema/protection";
|
|
8
|
+
import type { CommentAuthor } from "../commands/comments/model";
|
|
9
|
+
export interface DocumentProtectionOptions {
|
|
10
|
+
protection: EditingProtection;
|
|
11
|
+
/** Whose comments are being written. Null where none are, a reader above all */
|
|
12
|
+
author: CommentAuthor | null;
|
|
13
|
+
editableComments: EditableComments;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The transaction that puts the editor under another protection, which is how a mode is switched
|
|
17
|
+
* on a document already open: `reconfigure` keeps the state a plugin of the same key already
|
|
18
|
+
* holds, so a new plugin instance would not do it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function setProtection(tr: Transaction, options: DocumentProtectionOptions): Transaction;
|
|
21
|
+
export declare function documentProtection(options: DocumentProtectionOptions): Plugin<ProtectionState>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/editor/plugins/documentProtection.ts
|
|
2
|
+
import { Plugin } from "prosemirror-state";
|
|
3
|
+
import { protectionKey } from "../../schema/protectionState.js";
|
|
4
|
+
function rulesOf({
|
|
5
|
+
protection,
|
|
6
|
+
author,
|
|
7
|
+
editableComments
|
|
8
|
+
}) {
|
|
9
|
+
return { protection, authorId: author?.id ?? null, editableComments };
|
|
10
|
+
}
|
|
11
|
+
var PROTECTIONS = [
|
|
12
|
+
"none",
|
|
13
|
+
"readOnly",
|
|
14
|
+
"comments"
|
|
15
|
+
];
|
|
16
|
+
var EDITABLE_COMMENTS = ["own", "all"];
|
|
17
|
+
function isOneOf(values, candidate) {
|
|
18
|
+
return typeof candidate === "string" && values.some((value) => value === candidate);
|
|
19
|
+
}
|
|
20
|
+
function isRules(value) {
|
|
21
|
+
if (typeof value !== "object" || value === null) return false;
|
|
22
|
+
const authorId = Reflect.get(value, "authorId");
|
|
23
|
+
return isOneOf(PROTECTIONS, Reflect.get(value, "protection")) && (authorId === null || typeof authorId === "string") && isOneOf(EDITABLE_COMMENTS, Reflect.get(value, "editableComments"));
|
|
24
|
+
}
|
|
25
|
+
function setProtection(tr, options) {
|
|
26
|
+
return tr.setMeta(protectionKey, rulesOf(options));
|
|
27
|
+
}
|
|
28
|
+
function documentProtection(options) {
|
|
29
|
+
const rules = rulesOf(options);
|
|
30
|
+
return new Plugin({
|
|
31
|
+
key: protectionKey,
|
|
32
|
+
state: {
|
|
33
|
+
init: () => rules,
|
|
34
|
+
apply: (transaction, value) => {
|
|
35
|
+
const next = transaction.getMeta(protectionKey);
|
|
36
|
+
return isRules(next) ? next : value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
documentProtection,
|
|
43
|
+
setProtection
|
|
44
|
+
};
|
|
@@ -3,6 +3,7 @@ import { isHistoryTransaction } from "prosemirror-history";
|
|
|
3
3
|
import { Plugin, PluginKey } from "prosemirror-state";
|
|
4
4
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
5
5
|
import { replacementShut } from "../../schema/locks.js";
|
|
6
|
+
import { editsShut } from "../../schema/protectionState.js";
|
|
6
7
|
import {
|
|
7
8
|
hasResolvableClipboardImages,
|
|
8
9
|
resolveClipboardImages
|
|
@@ -222,7 +223,9 @@ function imagePaste() {
|
|
|
222
223
|
props: {
|
|
223
224
|
decorations: (state) => imagePasteKey.getState(state)?.decorations,
|
|
224
225
|
handlePaste(view, event) {
|
|
225
|
-
if (replacementShut(view.state.selection, view.state.doc))
|
|
226
|
+
if (editsShut(view.state) || replacementShut(view.state.selection, view.state.doc)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
226
229
|
if (view.state.selection.ranges.length > 1) return false;
|
|
227
230
|
const html = event.clipboardData?.getData("text/html") ?? "";
|
|
228
231
|
if (!hasResolvableClipboardImages(view.dom.ownerDocument, html)) {
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keys not handled here are taken by ProseMirror's base keymap.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type Command, Plugin } from "prosemirror-state";
|
|
5
|
+
/**
|
|
6
|
+
* Keeps undo and redo on their keys where the keymap above cannot reach them.
|
|
7
|
+
*
|
|
8
|
+
* A view that took editing away drops every keydown before the keymap plugin sees it
|
|
9
|
+
* (`view.editable || !(event.type in editHandlers)` in prosemirror-view), so a commenter, who is
|
|
10
|
+
* given no caret, would have no way to take a comment back. A DOM handler is asked ahead of that
|
|
11
|
+
* check. An editable view leaves the keys to the keymap plugin, which would otherwise run them a
|
|
12
|
+
* second time.
|
|
13
|
+
*/
|
|
14
|
+
export declare function historyKeys(): Plugin;
|
|
5
15
|
export declare const docxKeymap: Record<string, Command>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// src/editor/plugins/keymap.ts
|
|
2
2
|
import { chainCommands } from "prosemirror-commands";
|
|
3
3
|
import { undoInputRule } from "prosemirror-inputrules";
|
|
4
|
+
import { keydownHandler } from "prosemirror-keymap";
|
|
5
|
+
import { Plugin } from "prosemirror-state";
|
|
4
6
|
import { goToNextCell } from "prosemirror-tables";
|
|
5
7
|
import { canSplit } from "prosemirror-transform";
|
|
6
8
|
import { toParagraphFormat } from "../../model/format.js";
|
|
@@ -55,6 +57,25 @@ var preserveTableFollowingParagraph = (state) => {
|
|
|
55
57
|
const paragraphIndex = $from.index(0);
|
|
56
58
|
return paragraphIndex > 0 && state.doc.child(paragraphIndex - 1).type.spec.tableRole === "table";
|
|
57
59
|
};
|
|
60
|
+
var historyKeymap = {
|
|
61
|
+
"Mod-z": undo,
|
|
62
|
+
"Mod-y": redo,
|
|
63
|
+
"Shift-Mod-z": redo
|
|
64
|
+
};
|
|
65
|
+
function historyKeys() {
|
|
66
|
+
const runHistoryKeys = keydownHandler(historyKeymap);
|
|
67
|
+
return new Plugin({
|
|
68
|
+
props: {
|
|
69
|
+
handleDOMEvents: {
|
|
70
|
+
keydown: (view, event) => {
|
|
71
|
+
if (view.editable || !runHistoryKeys(view, event)) return false;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
58
79
|
var docxKeymap = {
|
|
59
80
|
Enter: chainCommands(leaveEmptyListItem, splitParagraph),
|
|
60
81
|
"Shift-Enter": insertLineBreak,
|
|
@@ -62,9 +83,7 @@ var docxKeymap = {
|
|
|
62
83
|
"Mod-Enter": insertPageBreak,
|
|
63
84
|
// The paragraph keeps adjacent tables separate without changing imported documents.
|
|
64
85
|
Backspace: chainCommands(preserveTableFollowingParagraph, undoInputRule),
|
|
65
|
-
|
|
66
|
-
"Mod-y": redo,
|
|
67
|
-
"Shift-Mod-z": redo,
|
|
86
|
+
...historyKeymap,
|
|
68
87
|
"Mod-b": toggleBold,
|
|
69
88
|
"Mod-i": toggleItalic,
|
|
70
89
|
"Mod-u": toggleUnderline,
|
|
@@ -90,5 +109,6 @@ var docxKeymap = {
|
|
|
90
109
|
"Shift-ArrowRight": moveAcrossTab("right", true)
|
|
91
110
|
};
|
|
92
111
|
export {
|
|
93
|
-
docxKeymap
|
|
112
|
+
docxKeymap,
|
|
113
|
+
historyKeys
|
|
94
114
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rejects transactions disallowed by `schema/locks
|
|
3
|
-
*
|
|
2
|
+
* Rejects transactions disallowed by the guard in `schema/locks`, which answers for the locks the
|
|
3
|
+
* document carries and for the protection the editor runs under (`schema/protection`) alike. A
|
|
4
|
+
* rejected IME edit also ends the browser composition on the next frame so ProseMirror cannot
|
|
5
|
+
* remain stuck in composing state.
|
|
4
6
|
*/
|
|
5
7
|
import { Plugin } from "prosemirror-state";
|
|
6
8
|
export declare function lockedContent(): Plugin;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
PluginKey,
|
|
5
5
|
TextSelection
|
|
6
6
|
} from "prosemirror-state";
|
|
7
|
+
import { editsShut } from "../../schema/protectionState.js";
|
|
7
8
|
var CLOSE = "close";
|
|
8
9
|
var menuKey = new PluginKey("docxEditorTableMenu");
|
|
9
10
|
function isRecord(value) {
|
|
@@ -67,7 +68,7 @@ function tableContextMenu() {
|
|
|
67
68
|
props: {
|
|
68
69
|
handleDOMEvents: {
|
|
69
70
|
contextmenu(view, event) {
|
|
70
|
-
if (
|
|
71
|
+
if (editsShut(view.state)) return false;
|
|
71
72
|
const cellPos = cellAtEvent(view, event.target);
|
|
72
73
|
if (cellPos === null) return false;
|
|
73
74
|
event.preventDefault();
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TextSelection
|
|
6
6
|
} from "prosemirror-state";
|
|
7
7
|
import { CellSelection } from "prosemirror-tables";
|
|
8
|
+
import { editingProtection, editsShut } from "../../schema/protectionState.js";
|
|
8
9
|
var CLOSE = "close";
|
|
9
10
|
var menuKey = new PluginKey("docxEditorTextMenu");
|
|
10
11
|
function isRecord(value) {
|
|
@@ -44,6 +45,7 @@ function isInSelection(state, spot) {
|
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
function forTableMenu(view, target, spot) {
|
|
48
|
+
if (editsShut(view.state)) return false;
|
|
47
49
|
const aboutCells = view.state.selection instanceof CellSelection || !isInSelection(view.state, spot);
|
|
48
50
|
return aboutCells && isInTableCell(view, target);
|
|
49
51
|
}
|
|
@@ -71,9 +73,12 @@ function textContextMenu() {
|
|
|
71
73
|
props: {
|
|
72
74
|
handleDOMEvents: {
|
|
73
75
|
contextmenu(view, event) {
|
|
74
|
-
if (
|
|
76
|
+
if (editingProtection(view.state) === "readOnly") return false;
|
|
75
77
|
if (!isInEditor(view, event.target)) return false;
|
|
76
78
|
const spot = clickedSpot(view, event);
|
|
79
|
+
if (editsShut(view.state) && !isInSelection(view.state, spot)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
77
82
|
if (forTableMenu(view, event.target, spot)) return false;
|
|
78
83
|
event.preventDefault();
|
|
79
84
|
view.dispatch(openMenu(view.state, event, spot));
|