@portone/docx-editor 0.4.0 → 0.5.1
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 +201 -0
- package/CONTRIBUTING.md +2 -0
- package/assets/editor.png +0 -0
- package/dist/DocxEditor.d.ts +1 -1
- package/dist/DocxEditor.js +44 -25
- package/dist/docx/cloning.js +4 -9
- package/dist/docx/commentOnlyChange.d.ts +5 -3
- package/dist/docx/comments/grammar.d.ts +27 -10
- package/dist/docx/comments/grammar.js +18 -67
- package/dist/docx/comments/model.d.ts +5 -6
- package/dist/docx/comments/model.js +0 -9
- package/dist/docx/comments/parts.d.ts +16 -10
- package/dist/docx/comments/parts.js +65 -13
- package/dist/docx/comments/people.d.ts +11 -3
- package/dist/docx/comments/people.js +20 -6
- package/dist/docx/comments/policy.js +7 -1
- package/dist/docx/comments/reading.d.ts +4 -2
- package/dist/docx/comments/reading.js +1 -14
- package/dist/docx/comments/writing.js +65 -28
- package/dist/docx/exportDocx.js +10 -14
- package/dist/docx/exportRefs.d.ts +8 -3
- package/dist/docx/exportRefs.js +2 -1
- package/dist/docx/fidelity.js +24 -10
- package/dist/docx/fields.d.ts +42 -0
- package/dist/docx/fields.js +76 -0
- package/dist/docx/formatting/styles.d.ts +0 -2
- package/dist/docx/formatting/styles.js +2 -15
- package/dist/docx/headersFooters.d.ts +89 -20
- package/dist/docx/headersFooters.js +163 -172
- package/dist/docx/identities.js +17 -26
- package/dist/docx/importDocx.d.ts +1 -1
- package/dist/docx/importDocx.js +100 -48
- package/dist/docx/importParagraph.d.ts +18 -5
- package/dist/docx/importParagraph.js +75 -128
- package/dist/docx/importPolicy.d.ts +70 -0
- package/dist/docx/importPolicy.js +195 -0
- package/dist/docx/importPreserved.d.ts +30 -0
- package/dist/docx/importPreserved.js +54 -0
- package/dist/docx/importTable.js +56 -12
- package/dist/docx/invariants.js +16 -18
- package/dist/docx/notes.d.ts +6 -1
- package/dist/docx/notes.js +6 -16
- package/dist/docx/pageGeometry.d.ts +5 -14
- package/dist/docx/pageGeometry.js +0 -12
- package/dist/docx/protectionPolicy.d.ts +13 -5
- package/dist/docx/protectionPolicy.js +32 -22
- package/dist/docx/scan.d.ts +18 -8
- package/dist/docx/scan.js +17 -11
- package/dist/docx/sections.d.ts +126 -0
- package/dist/docx/sections.js +207 -0
- package/dist/docx/serializeBlock.d.ts +13 -4
- package/dist/docx/serializeBlock.js +16 -18
- package/dist/docx/serializeParagraph.d.ts +9 -4
- package/dist/docx/serializeParagraph.js +23 -58
- package/dist/docx/serializePreserved.d.ts +14 -0
- package/dist/docx/serializePreserved.js +24 -0
- package/dist/docx/serializeStory.d.ts +17 -0
- package/dist/docx/serializeStory.js +17 -0
- package/dist/docx/serializeTable.js +10 -13
- package/dist/docx/session.d.ts +23 -6
- package/dist/docx/session.js +26 -5
- package/dist/docx/story.d.ts +140 -0
- package/dist/docx/story.js +237 -0
- package/dist/docx/storyProjection.js +6 -1
- package/dist/docx/tableTemplate.d.ts +25 -0
- package/dist/docx/tableTemplate.js +30 -16
- package/dist/docx/wrappers.d.ts +67 -0
- package/dist/docx/wrappers.js +117 -0
- package/dist/editor/clipboard/blockReaders.d.ts +74 -0
- package/dist/editor/clipboard/blockReaders.js +193 -0
- package/dist/editor/clipboard/htmlReader.d.ts +26 -0
- package/dist/editor/clipboard/htmlReader.js +376 -0
- package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
- package/dist/editor/clipboard/inlineFormatting.js +1 -7
- package/dist/editor/clipboard/internalChannel.d.ts +57 -0
- package/dist/editor/clipboard/internalChannel.js +58 -0
- package/dist/editor/clipboard/normalizers.d.ts +99 -0
- package/dist/editor/clipboard/normalizers.js +199 -0
- package/dist/editor/clipboard/parser.d.ts +34 -0
- package/dist/editor/clipboard/parser.js +58 -0
- package/dist/editor/clipboard/plugin.d.ts +21 -0
- package/dist/editor/clipboard/plugin.js +188 -0
- package/dist/editor/clipboard/readContext.d.ts +34 -0
- package/dist/editor/clipboard/readContext.js +25 -0
- package/dist/editor/clipboard/readers.d.ts +33 -0
- package/dist/editor/clipboard/readers.js +37 -0
- package/dist/editor/clipboard/source.d.ts +12 -0
- package/dist/editor/clipboard/source.js +21 -0
- package/dist/editor/commands/comments/editing.d.ts +26 -6
- package/dist/editor/commands/comments/editing.js +177 -106
- package/dist/editor/commands/comments/model.d.ts +34 -17
- package/dist/editor/commands/comments/model.js +11 -0
- package/dist/editor/commands/comments/reading.d.ts +5 -3
- package/dist/editor/commands/comments/reading.js +8 -58
- package/dist/editor/commands/index.d.ts +2 -2
- package/dist/editor/commands/index.js +2 -0
- package/dist/editor/commands/linkCommands.js +20 -5
- package/dist/editor/commands/listCommands.d.ts +6 -0
- package/dist/editor/commands/listCommands.js +3 -2
- package/dist/editor/commands/lockCommands.js +59 -16
- package/dist/editor/commands/noteQueries.d.ts +22 -5
- package/dist/editor/commands/noteQueries.js +34 -6
- package/dist/editor/commands/paragraphCommands.js +2 -2
- package/dist/editor/createEditor.js +13 -3
- package/dist/editor/documentStyles.d.ts +26 -4
- package/dist/editor/documentStyles.js +9 -4
- package/dist/editor/imageFiles.d.ts +2 -2
- package/dist/editor/imageFiles.js +2 -0
- package/dist/editor/insertTable.d.ts +1 -1
- package/dist/editor/insertTable.js +2 -2
- package/dist/editor/plainText.d.ts +12 -0
- package/dist/editor/plainText.js +2 -1
- package/dist/editor/plugins/columnResize.js +6 -3
- package/dist/editor/plugins/commentComposer.d.ts +28 -0
- package/dist/editor/plugins/commentComposer.js +46 -0
- package/dist/editor/plugins/commentDecorations.d.ts +29 -2
- package/dist/editor/plugins/commentDecorations.js +89 -15
- package/dist/editor/plugins/documentProjection.d.ts +31 -0
- package/dist/editor/plugins/documentProjection.js +22 -0
- package/dist/editor/plugins/imagePaste.js +15 -8
- package/dist/editor/plugins/linkPanel.d.ts +7 -3
- package/dist/editor/plugins/linkPanel.js +13 -27
- package/dist/editor/plugins/panelState.d.ts +67 -0
- package/dist/editor/plugins/panelState.js +54 -0
- package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
- package/dist/editor/plugins/tableContextMenu.js +30 -47
- package/dist/editor/plugins/textContextMenu.d.ts +1 -1
- package/dist/editor/plugins/textContextMenu.js +30 -47
- package/dist/ooxml/fragment.d.ts +11 -0
- package/dist/ooxml/fragment.js +14 -4
- package/dist/ooxml/image.d.ts +3 -4
- package/dist/ooxml/image.js +50 -5
- package/dist/ooxml/names.d.ts +6 -0
- package/dist/ooxml/names.js +2 -0
- package/dist/ooxml/props.d.ts +10 -0
- package/dist/ooxml/props.js +28 -4
- package/dist/ooxml/rangeMarkers.d.ts +5 -0
- package/dist/ooxml/rangeMarkers.js +24 -0
- package/dist/page/PageGuides.d.ts +7 -3
- package/dist/page/PageGuides.js +13 -24
- package/dist/page/pageLayout.d.ts +64 -5
- package/dist/page/pageLayout.js +90 -29
- package/dist/page/usePageLayout.d.ts +14 -4
- package/dist/page/usePageLayout.js +19 -20
- package/dist/schema/attrRoles.js +46 -23
- package/dist/schema/clipboard.d.ts +72 -0
- package/dist/schema/clipboard.js +288 -0
- package/dist/schema/docxSchema.d.ts +9 -1
- package/dist/schema/docxSchema.js +217 -165
- package/dist/schema/editGuard.d.ts +1 -1
- package/dist/schema/guards.js +2 -2
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/locks.d.ts +3 -0
- package/dist/schema/locks.js +29 -12
- package/dist/schema/preservedFragments.d.ts +12 -0
- package/dist/schema/preservedFragments.js +38 -0
- package/dist/schema/preservedGuards.d.ts +9 -7
- package/dist/schema/preservedGuards.js +112 -11
- package/dist/schema/protection.d.ts +34 -5
- package/dist/schema/protection.js +42 -14
- package/dist/schema/rendering.d.ts +14 -0
- package/dist/schema/rendering.js +27 -12
- package/dist/schema/stories.d.ts +49 -0
- package/dist/schema/stories.js +78 -0
- package/dist/schema/wrappers.d.ts +58 -0
- package/dist/schema/wrappers.js +77 -0
- package/dist/styles/classNames.d.ts +3 -5
- package/dist/styles/classNames.js +3 -5
- package/dist/styles.css +74 -12
- package/dist/table/resize.d.ts +5 -3
- package/dist/ui/CommentsPanel.d.ts +1 -3
- package/dist/ui/CommentsPanel.js +24 -15
- package/dist/ui/NotesPanel.js +2 -2
- package/dist/ui/TextMenu.d.ts +1 -3
- package/dist/ui/TextMenu.js +10 -6
- package/dist/ui/comments/CommentComposer.d.ts +6 -1
- package/dist/ui/comments/CommentComposer.js +26 -3
- package/package.json +5 -4
- package/dist/editor/externalClipboard.d.ts +0 -22
- package/dist/editor/externalClipboard.js +0 -486
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EditorState } from "prosemirror-state";
|
|
2
|
+
import type { FormattingContext, ParagraphStyleOption } from "../../docx/formatting";
|
|
3
|
+
import type { PageGeometry } from "../../docx/pageGeometry";
|
|
4
|
+
import type { ImageToInsert } from "../insertImage";
|
|
5
|
+
import type { HtmlSource } from "./source";
|
|
6
|
+
/**
|
|
7
|
+
* Everything a piece of HTML is read against.
|
|
8
|
+
*
|
|
9
|
+
* The reader answers about the document it is read into - which styles it may name, which list
|
|
10
|
+
* numbers are taken, whether a list may be started at all - and nothing about the editor holding
|
|
11
|
+
* it. Gathering those in one place is what keeps the reader off the editing state, so the same
|
|
12
|
+
* reading can be asked for from a paste, from a drop, and from a test.
|
|
13
|
+
*/
|
|
14
|
+
export interface HtmlReadContext {
|
|
15
|
+
/** The document the markup is read into an element of */
|
|
16
|
+
document: Document;
|
|
17
|
+
formatting: FormattingContext;
|
|
18
|
+
/** The paper a block read into the document lands on, which is the width a pasted table takes */
|
|
19
|
+
geometry: PageGeometry;
|
|
20
|
+
paragraphStyles: readonly ParagraphStyleOption[];
|
|
21
|
+
numbering: {
|
|
22
|
+
used: ReadonlySet<number>;
|
|
23
|
+
canCreate: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** The images already loaded for this read, by the token the markup carries */
|
|
26
|
+
images: ReadonlyMap<string, ImageToInsert>;
|
|
27
|
+
/**
|
|
28
|
+
* The application that wrote the markup being read (`./source`). It is a property of the markup
|
|
29
|
+
* and not of the document, so a context built before any markup is in hand names no application
|
|
30
|
+
* and `readHtml` fills it in from what it is handed.
|
|
31
|
+
*/
|
|
32
|
+
source: HtmlSource;
|
|
33
|
+
}
|
|
34
|
+
export declare function readContextOf(state: EditorState, document: Document, images?: ReadonlyMap<string, ImageToInsert>): HtmlReadContext;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/editor/clipboard/readContext.ts
|
|
2
|
+
import { numIdsIn } from "../commands/listCommands.js";
|
|
3
|
+
import {
|
|
4
|
+
documentFormatting,
|
|
5
|
+
documentParagraphStyles,
|
|
6
|
+
sectionGeometryAt
|
|
7
|
+
} from "../documentStyles.js";
|
|
8
|
+
import { canStartNewList } from "../plugins/numberingDecorations.js";
|
|
9
|
+
function readContextOf(state, document, images = /* @__PURE__ */ new Map()) {
|
|
10
|
+
return {
|
|
11
|
+
document,
|
|
12
|
+
formatting: documentFormatting(state),
|
|
13
|
+
geometry: sectionGeometryAt(state, state.selection.from),
|
|
14
|
+
paragraphStyles: documentParagraphStyles(state),
|
|
15
|
+
numbering: {
|
|
16
|
+
used: numIdsIn(state.doc),
|
|
17
|
+
canCreate: canStartNewList(state)
|
|
18
|
+
},
|
|
19
|
+
images,
|
|
20
|
+
source: "unknown"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
readContextOf
|
|
25
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ResolvedPos, Slice } from "prosemirror-model";
|
|
2
|
+
import { type PastedContent } from "./htmlReader";
|
|
3
|
+
import type { HtmlReadContext } from "./readContext";
|
|
4
|
+
/** What one reader is handed: the markup on the clipboard, and the document it is read into */
|
|
5
|
+
export interface ClipboardInput {
|
|
6
|
+
dom: Node;
|
|
7
|
+
/** The name the markup carries for a slice this editor kept (`./internalChannel`). Null when it carries none */
|
|
8
|
+
token: string | null;
|
|
9
|
+
/** The open document the markup is being read into. Null for a state built without one */
|
|
10
|
+
sessionId: string | null;
|
|
11
|
+
context: HtmlReadContext;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One way of reading a clipboard. The chain is tried in order and the first reader to answer
|
|
15
|
+
* decides what goes in, so a reader that does not recognize what it was handed answers nothing.
|
|
16
|
+
*/
|
|
17
|
+
export type ClipboardReader = (input: ClipboardInput) => PastedContent | null;
|
|
18
|
+
/**
|
|
19
|
+
* A copy made in this same open document, handed back as the slice it was.
|
|
20
|
+
*
|
|
21
|
+
* It leads the chain because it is the only reader that loses nothing: everything below reads
|
|
22
|
+
* markup, and markup is all a copy from anywhere else amounts to here.
|
|
23
|
+
*/
|
|
24
|
+
export declare const internalSliceReader: ClipboardReader;
|
|
25
|
+
/** Everything the editor did not write itself, read for the formatting the markup states */
|
|
26
|
+
export declare const foreignHtmlReader: ClipboardReader;
|
|
27
|
+
export declare const DEFAULT_READERS: readonly ClipboardReader[];
|
|
28
|
+
/**
|
|
29
|
+
* Plain text as content of this document: docx has no character for a line break or a tab stop,
|
|
30
|
+
* so those become the elements it does have, and the rest keeps the formatting in force where it
|
|
31
|
+
* is put.
|
|
32
|
+
*/
|
|
33
|
+
export declare function plainTextSlice(text: string, $context: ResolvedPos): Slice;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/editor/clipboard/readers.ts
|
|
2
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
3
|
+
import { NO_NEW_LISTS } from "../../numbering/listRegistry.js";
|
|
4
|
+
import { docxSchema } from "../../schema/index.js";
|
|
5
|
+
import { toPieces } from "../plainText.js";
|
|
6
|
+
import { readHtml } from "./htmlReader.js";
|
|
7
|
+
import { recallCopied } from "./internalChannel.js";
|
|
8
|
+
var internalSliceReader = ({ token, sessionId }) => {
|
|
9
|
+
const recalled = recallCopied(token, sessionId);
|
|
10
|
+
return recalled === null ? null : { ...recalled, newLists: NO_NEW_LISTS };
|
|
11
|
+
};
|
|
12
|
+
var foreignHtmlReader = ({ dom, context }) => dom instanceof Element || dom instanceof DocumentFragment ? readHtml(dom, context) : null;
|
|
13
|
+
var DEFAULT_READERS = [
|
|
14
|
+
internalSliceReader,
|
|
15
|
+
foreignHtmlReader
|
|
16
|
+
];
|
|
17
|
+
function plainTextSlice(text, $context) {
|
|
18
|
+
const marks = $context.marks();
|
|
19
|
+
const nodes = toPieces(text).map((piece) => {
|
|
20
|
+
if ("text" in piece) return docxSchema.text(piece.text, marks);
|
|
21
|
+
if (piece.node === "hardBreak") {
|
|
22
|
+
return docxSchema.nodes.hardBreak.create(null, null, marks);
|
|
23
|
+
}
|
|
24
|
+
const beside = marks.filter((mark) => mark.type !== docxSchema.marks.tab);
|
|
25
|
+
return docxSchema.text(
|
|
26
|
+
" ",
|
|
27
|
+
docxSchema.marks.tab.create().addToSet(beside)
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
return nodes.length === 0 ? Slice.empty : new Slice(Fragment.fromArray(nodes), 0, 0);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
DEFAULT_READERS,
|
|
34
|
+
foreignHtmlReader,
|
|
35
|
+
internalSliceReader,
|
|
36
|
+
plainTextSlice
|
|
37
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which application wrote the markup on the clipboard.
|
|
3
|
+
*
|
|
4
|
+
* Every writer of HTML says so somewhere - in a generator meta element, in a class prefix, in the
|
|
5
|
+
* id it wraps a copy in - and what it says decides which readings apply: Word's list paragraphs
|
|
6
|
+
* are lists rather than paragraphs beginning with a bullet character, and its `<o:p>` markers are
|
|
7
|
+
* nothing at all. A reading recognizing no writer stays with what the markup itself states.
|
|
8
|
+
*/
|
|
9
|
+
/** The writers whose markup is read differently from plain HTML */
|
|
10
|
+
export type HtmlSource = "editor" | "word" | "google-docs" | "libreoffice" | "unknown";
|
|
11
|
+
/** The application the markup under this element was written by */
|
|
12
|
+
export declare function detectHtmlSource(root: ParentNode): HtmlSource;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/editor/clipboard/source.ts
|
|
2
|
+
import { editorClassNames } from "../../styles/classNames.js";
|
|
3
|
+
import { INTERNAL_TOKEN_ATTRIBUTE } from "./internalChannel.js";
|
|
4
|
+
var SIGNATURES = [
|
|
5
|
+
["editor", `[${INTERNAL_TOKEN_ATTRIBUTE}], .${editorClassNames.paragraph}`],
|
|
6
|
+
["word", 'meta[name="Generator" i][content^="Microsoft Word" i], .MsoNormal'],
|
|
7
|
+
["google-docs", '[id^="docs-internal-guid-"]'],
|
|
8
|
+
[
|
|
9
|
+
"libreoffice",
|
|
10
|
+
'meta[name="generator" i][content*="LibreOffice" i], meta[name="generator" i][content*="OpenOffice" i]'
|
|
11
|
+
]
|
|
12
|
+
];
|
|
13
|
+
function detectHtmlSource(root) {
|
|
14
|
+
for (const [source, selector] of SIGNATURES) {
|
|
15
|
+
if (root.querySelector(selector) !== null) return source;
|
|
16
|
+
}
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
detectHtmlSource
|
|
21
|
+
};
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
/** Edits Word comments through their range markers and reference nodes. */
|
|
2
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
2
3
|
import { type Command, type EditorState } from "prosemirror-state";
|
|
3
|
-
import type
|
|
4
|
-
/** Adds a plain-text comment to the current text selection. */
|
|
5
|
-
export declare function addComment(comment: NewComment): Command;
|
|
4
|
+
import { type CommentRange, type NewComment } from "./model";
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Adds a plain-text comment to a stretch of text: the one given, or the current text selection.
|
|
7
|
+
*
|
|
8
|
+
* A composer that was opened over one stretch and submitted later names it, so that what the
|
|
9
|
+
* comment marks is the text it was written about rather than wherever the caret has since gone.
|
|
10
|
+
*/
|
|
11
|
+
export declare function addComment(comment: NewComment, at?: CommentRange): Command;
|
|
12
|
+
/**
|
|
13
|
+
* Whether that stretch of text, or the current selection, can receive a comment.
|
|
8
14
|
* This is the command itself asked without a dispatch, over a comment standing in for the one the
|
|
9
15
|
* user would write, so the button and the click cannot answer differently.
|
|
10
16
|
*/
|
|
11
|
-
export declare function canAddComment(state: EditorState): boolean;
|
|
17
|
+
export declare function canAddComment(state: EditorState, at?: CommentRange): boolean;
|
|
12
18
|
/** Replaces the plain-text body of one comment, retaining its author and anchor. */
|
|
13
19
|
export declare function updateComment(id: string, text: string): Command;
|
|
20
|
+
/**
|
|
21
|
+
* Replaces the body of one comment with a formatted one, retaining its author and anchor.
|
|
22
|
+
*
|
|
23
|
+
* The body is a document of `docxSchema`, the same schema the page is edited in, so a paragraph
|
|
24
|
+
* style, a bold run and a second paragraph all survive being written, exported and read back.
|
|
25
|
+
* `updateComment` is the same command for a body that is only text.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setCommentBody(id: string, body: PMNode): Command;
|
|
14
28
|
/** Marks a comment thread resolved or open without deleting it. */
|
|
15
29
|
export declare function setCommentResolved(id: string, resolved: boolean): Command;
|
|
16
30
|
/** Adds a plain-text reply and reopens the thread when it was resolved. */
|
|
@@ -19,7 +33,13 @@ export declare function addCommentReply(id: string, reply: NewComment): Command;
|
|
|
19
33
|
export declare function updateCommentReply(commentId: string, replyId: string, text: string): Command;
|
|
20
34
|
/** Removes one reply while retaining its root comment and anchor. */
|
|
21
35
|
export declare function removeCommentReply(commentId: string, replyId: string): Command;
|
|
22
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Removes a comment's range markers, reference and Comments-part entry.
|
|
38
|
+
*
|
|
39
|
+
* What the comment and its replies said goes with them: a body is a story on the document node,
|
|
40
|
+
* and one left behind for a comment nothing refers to would ride every later comparison of the
|
|
41
|
+
* document without standing for anything.
|
|
42
|
+
*/
|
|
23
43
|
export declare function removeComment(id: string): Command;
|
|
24
44
|
/** Selects the text anchored by a comment, or places the caret at a point comment. */
|
|
25
45
|
export declare function selectComment(id: string): Command;
|
|
@@ -3,16 +3,40 @@ import {
|
|
|
3
3
|
TextSelection
|
|
4
4
|
} from "prosemirror-state";
|
|
5
5
|
import { commentParaId } from "../../../docx/comments.js";
|
|
6
|
+
import {
|
|
7
|
+
sameStory,
|
|
8
|
+
setStory,
|
|
9
|
+
storyFromText,
|
|
10
|
+
storyKey,
|
|
11
|
+
storyOf,
|
|
12
|
+
storyText,
|
|
13
|
+
withoutStories
|
|
14
|
+
} from "../../../docx/story.js";
|
|
6
15
|
import { docxSchema } from "../../../schema/index.js";
|
|
7
16
|
import { guardedCommand } from "../../../schema/guards.js";
|
|
17
|
+
import { isWrapperType } from "../../../schema/wrappers.js";
|
|
8
18
|
import {
|
|
9
19
|
reservedCommentIds,
|
|
10
20
|
reservedCommentParaIds
|
|
11
21
|
} from "../../editorDocument.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
22
|
+
import {
|
|
23
|
+
repliesAttr,
|
|
24
|
+
stringAttr
|
|
25
|
+
} from "./model.js";
|
|
26
|
+
import { commentById } from "./reading.js";
|
|
27
|
+
function anchorableRange(doc, { from, to }) {
|
|
28
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || from >= to || from < 0 || to > doc.content.size)
|
|
29
|
+
return null;
|
|
30
|
+
const $from = doc.resolve(from);
|
|
31
|
+
if (!$from.sameParent(doc.resolve(to))) return null;
|
|
32
|
+
if ($from.parent.type.name !== "paragraph") return null;
|
|
33
|
+
return { from, to };
|
|
34
|
+
}
|
|
35
|
+
function commentedRange(state, at) {
|
|
36
|
+
if (at) return anchorableRange(state.doc, at);
|
|
14
37
|
const { selection } = state;
|
|
15
|
-
|
|
38
|
+
if (!(selection instanceof TextSelection)) return null;
|
|
39
|
+
return anchorableRange(state.doc, selection);
|
|
16
40
|
}
|
|
17
41
|
function nextCommentId(state) {
|
|
18
42
|
const taken = /* @__PURE__ */ new Set();
|
|
@@ -70,17 +94,16 @@ function wrapperMarksAt(state, pos, side) {
|
|
|
70
94
|
const resolved = state.doc.resolve(pos);
|
|
71
95
|
const adjacent = side === "before" ? resolved.nodeBefore : resolved.nodeAfter;
|
|
72
96
|
return (adjacent?.marks ?? resolved.marks()).filter(
|
|
73
|
-
(mark) => mark.type
|
|
97
|
+
(mark) => isWrapperType(mark.type)
|
|
74
98
|
);
|
|
75
99
|
}
|
|
76
|
-
function addCommentTransaction(state, comment) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
100
|
+
function addCommentTransaction(state, comment, at) {
|
|
101
|
+
const range = commentedRange(state, at);
|
|
102
|
+
if (range === null || comment.text.trim().length === 0) return null;
|
|
80
103
|
const id = nextCommentId(state);
|
|
81
104
|
const date = comment.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
82
105
|
const paraId = nextCommentParaId(state, `comment-${id}-${date}`);
|
|
83
|
-
const { from, to } =
|
|
106
|
+
const { from, to } = range;
|
|
84
107
|
const startMarks = wrapperMarksAt(state, from, "after");
|
|
85
108
|
const endMarks = wrapperMarksAt(state, to, "before");
|
|
86
109
|
const end = docxSchema.nodes.commentEnd.create(
|
|
@@ -96,9 +119,6 @@ function addCommentTransaction(state, comment) {
|
|
|
96
119
|
authorId: comment.authorId ?? null,
|
|
97
120
|
initials: comment.initials ?? null,
|
|
98
121
|
date,
|
|
99
|
-
text: comment.text,
|
|
100
|
-
commentXml: null,
|
|
101
|
-
imported: false,
|
|
102
122
|
paraId,
|
|
103
123
|
resolved: false,
|
|
104
124
|
extensionXml: null,
|
|
@@ -113,39 +133,63 @@ function addCommentTransaction(state, comment) {
|
|
|
113
133
|
null,
|
|
114
134
|
startMarks
|
|
115
135
|
);
|
|
116
|
-
return
|
|
136
|
+
return setStory(
|
|
137
|
+
state.tr.insert(to, end).insert(to + 1, reference).insert(from, start),
|
|
138
|
+
storyKey("comment", id),
|
|
139
|
+
storyFromText(comment.text)
|
|
140
|
+
);
|
|
117
141
|
}
|
|
118
|
-
function addComment(comment) {
|
|
119
|
-
return guardedCommand((state) => addCommentTransaction(state, comment));
|
|
142
|
+
function addComment(comment, at) {
|
|
143
|
+
return guardedCommand((state) => addCommentTransaction(state, comment, at));
|
|
120
144
|
}
|
|
121
|
-
function canAddComment(state) {
|
|
122
|
-
return addComment(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
function canAddComment(state, at) {
|
|
146
|
+
return addComment(
|
|
147
|
+
{
|
|
148
|
+
text: "comment",
|
|
149
|
+
author: "Author",
|
|
150
|
+
date: "1970-01-01T00:00:00.000Z"
|
|
151
|
+
},
|
|
152
|
+
at
|
|
153
|
+
)(state);
|
|
127
154
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
...node.attrs,
|
|
137
|
-
text,
|
|
138
|
-
commentXml: null,
|
|
139
|
-
imported: false
|
|
140
|
-
});
|
|
141
|
-
changed = true;
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
});
|
|
145
|
-
return changed ? transaction : null;
|
|
155
|
+
function referenceAt(state, id) {
|
|
156
|
+
let at = null;
|
|
157
|
+
state.doc.descendants((node, pos) => {
|
|
158
|
+
if (at !== null) return false;
|
|
159
|
+
if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
|
|
160
|
+
at = pos;
|
|
161
|
+
}
|
|
162
|
+
return at === null;
|
|
146
163
|
});
|
|
164
|
+
return at;
|
|
165
|
+
}
|
|
166
|
+
function bodyTransaction(state, referenceId, bodyId, body) {
|
|
167
|
+
if (body.type !== docxSchema.nodes.doc) return null;
|
|
168
|
+
if (storyText(body).trim().length === 0) return null;
|
|
169
|
+
const at = referenceAt(state, referenceId);
|
|
170
|
+
const reference = at === null ? null : state.doc.nodeAt(at);
|
|
171
|
+
if (at === null || reference === null) return null;
|
|
172
|
+
const key = storyKey("comment", bodyId);
|
|
173
|
+
if (sameStory(storyOf(state.doc, key), body)) return null;
|
|
174
|
+
return setStory(state.tr, key, body).setNodeMarkup(
|
|
175
|
+
at,
|
|
176
|
+
null,
|
|
177
|
+
reference.attrs,
|
|
178
|
+
reference.marks
|
|
179
|
+
);
|
|
147
180
|
}
|
|
148
|
-
function
|
|
181
|
+
function textBodyTransaction(state, referenceId, bodyId, text) {
|
|
182
|
+
const held = storyOf(state.doc, storyKey("comment", bodyId));
|
|
183
|
+
if (held !== null && storyText(held) === text) return null;
|
|
184
|
+
return bodyTransaction(state, referenceId, bodyId, storyFromText(text));
|
|
185
|
+
}
|
|
186
|
+
function updateComment(id, text) {
|
|
187
|
+
return guardedCommand((state) => textBodyTransaction(state, id, id, text));
|
|
188
|
+
}
|
|
189
|
+
function setCommentBody(id, body) {
|
|
190
|
+
return guardedCommand((state) => bodyTransaction(state, id, id, body));
|
|
191
|
+
}
|
|
192
|
+
function updateReference(id, change, alongside = (tr) => tr) {
|
|
149
193
|
return guardedCommand((state) => {
|
|
150
194
|
let transaction = state.tr;
|
|
151
195
|
let changed = false;
|
|
@@ -160,7 +204,7 @@ function updateReference(id, change) {
|
|
|
160
204
|
}
|
|
161
205
|
return false;
|
|
162
206
|
});
|
|
163
|
-
return changed ? transaction : null;
|
|
207
|
+
return changed ? alongside(transaction) : null;
|
|
164
208
|
});
|
|
165
209
|
}
|
|
166
210
|
function setCommentResolved(id, resolved) {
|
|
@@ -179,84 +223,110 @@ function addCommentReply(id, reply) {
|
|
|
179
223
|
if (reply.text.trim().length === 0) return false;
|
|
180
224
|
const replyId = nextCommentId(state);
|
|
181
225
|
const date = reply.date ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
182
|
-
return updateReference(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
parentParaId
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
226
|
+
return updateReference(
|
|
227
|
+
id,
|
|
228
|
+
(node) => {
|
|
229
|
+
const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
|
|
230
|
+
const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
|
|
231
|
+
parentParaId
|
|
232
|
+
]);
|
|
233
|
+
return {
|
|
234
|
+
...node.attrs,
|
|
235
|
+
paraId: parentParaId,
|
|
236
|
+
resolved: false,
|
|
237
|
+
extensionXml: null,
|
|
238
|
+
threadImported: false,
|
|
239
|
+
replies: [
|
|
240
|
+
...repliesAttr(node.attrs.replies),
|
|
241
|
+
{
|
|
242
|
+
id: replyId,
|
|
243
|
+
author: reply.author,
|
|
244
|
+
authorId: reply.authorId ?? null,
|
|
245
|
+
initials: reply.initials ?? null,
|
|
246
|
+
date,
|
|
247
|
+
paraId,
|
|
248
|
+
parentParaId,
|
|
249
|
+
extensionXml: null
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
(tr) => setStory(tr, storyKey("comment", replyId), storyFromText(reply.text))
|
|
255
|
+
)(state, dispatch);
|
|
211
256
|
};
|
|
212
257
|
}
|
|
213
258
|
function updateCommentReply(commentId, replyId, text) {
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
return
|
|
220
|
-
...node.attrs,
|
|
221
|
-
replies: replies.map(
|
|
222
|
-
(reply, at) => at === index ? { ...reply, text, commentXml: null, imported: false } : reply
|
|
223
|
-
)
|
|
224
|
-
};
|
|
259
|
+
return guardedCommand((state) => {
|
|
260
|
+
const holdsReply = repliesOf(state, commentId).some(
|
|
261
|
+
(reply) => reply.id === replyId
|
|
262
|
+
);
|
|
263
|
+
if (!holdsReply) return null;
|
|
264
|
+
return textBodyTransaction(state, commentId, replyId, text);
|
|
225
265
|
});
|
|
226
266
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
const removedParaIds = new Set(
|
|
233
|
-
replies.filter((reply) => removedIds.has(reply.id)).map((reply) => reply.paraId)
|
|
234
|
-
);
|
|
235
|
-
let changed = true;
|
|
236
|
-
while (changed) {
|
|
237
|
-
changed = false;
|
|
238
|
-
for (const reply of replies) {
|
|
239
|
-
if (!removedIds.has(reply.id) && removedParaIds.has(reply.parentParaId)) {
|
|
240
|
-
removedIds.add(reply.id);
|
|
241
|
-
removedParaIds.add(reply.paraId);
|
|
242
|
-
changed = true;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
267
|
+
function repliesOf(state, commentId) {
|
|
268
|
+
let replies = [];
|
|
269
|
+
state.doc.descendants((node) => {
|
|
270
|
+
if (node.type.name !== "commentReference" || stringAttr(node.attrs.id) !== commentId) {
|
|
271
|
+
return true;
|
|
245
272
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
replies: replies.filter((reply) => !removedIds.has(reply.id)),
|
|
249
|
-
threadImported: false
|
|
250
|
-
};
|
|
273
|
+
replies = repliesAttr(node.attrs.replies);
|
|
274
|
+
return false;
|
|
251
275
|
});
|
|
276
|
+
return replies;
|
|
277
|
+
}
|
|
278
|
+
function dropBodies(ids) {
|
|
279
|
+
return (tr) => withoutStories(
|
|
280
|
+
tr,
|
|
281
|
+
Array.from(ids, (id) => storyKey("comment", id))
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function removeCommentReply(commentId, replyId) {
|
|
285
|
+
return (state, dispatch) => {
|
|
286
|
+
const removedReplyIds = /* @__PURE__ */ new Set();
|
|
287
|
+
return updateReference(
|
|
288
|
+
commentId,
|
|
289
|
+
(node) => {
|
|
290
|
+
const replies = repliesAttr(node.attrs.replies);
|
|
291
|
+
if (!replies.some((reply) => reply.id === replyId)) return null;
|
|
292
|
+
const removedIds = /* @__PURE__ */ new Set([replyId]);
|
|
293
|
+
const removedParaIds = new Set(
|
|
294
|
+
replies.filter((reply) => removedIds.has(reply.id)).map((reply) => reply.paraId)
|
|
295
|
+
);
|
|
296
|
+
let changed = true;
|
|
297
|
+
while (changed) {
|
|
298
|
+
changed = false;
|
|
299
|
+
for (const reply of replies) {
|
|
300
|
+
if (!removedIds.has(reply.id) && removedParaIds.has(reply.parentParaId)) {
|
|
301
|
+
removedIds.add(reply.id);
|
|
302
|
+
removedParaIds.add(reply.paraId);
|
|
303
|
+
changed = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const id of removedIds) removedReplyIds.add(id);
|
|
308
|
+
return {
|
|
309
|
+
...node.attrs,
|
|
310
|
+
replies: replies.filter((reply) => !removedIds.has(reply.id)),
|
|
311
|
+
threadImported: false
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
dropBodies(removedReplyIds)
|
|
315
|
+
)(state, dispatch);
|
|
316
|
+
};
|
|
252
317
|
}
|
|
253
318
|
function removeComment(id) {
|
|
254
319
|
return guardedCommand((state) => {
|
|
255
320
|
const positions = [];
|
|
321
|
+
const bodies = /* @__PURE__ */ new Set([id]);
|
|
256
322
|
state.doc.descendants((node, pos) => {
|
|
257
323
|
if ((node.type.name === "commentStart" || node.type.name === "commentEnd" || node.type.name === "commentReference") && stringAttr(node.attrs.id) === id) {
|
|
258
324
|
positions.push({ pos, size: node.nodeSize });
|
|
259
325
|
}
|
|
326
|
+
if (node.type.name === "commentReference" && stringAttr(node.attrs.id) === id) {
|
|
327
|
+
for (const reply of repliesAttr(node.attrs.replies))
|
|
328
|
+
bodies.add(reply.id);
|
|
329
|
+
}
|
|
260
330
|
return true;
|
|
261
331
|
});
|
|
262
332
|
if (positions.length === 0) return null;
|
|
@@ -264,12 +334,12 @@ function removeComment(id) {
|
|
|
264
334
|
for (const marker of positions.sort((a, b) => b.pos - a.pos)) {
|
|
265
335
|
transaction.delete(marker.pos, marker.pos + marker.size);
|
|
266
336
|
}
|
|
267
|
-
return transaction;
|
|
337
|
+
return dropBodies(bodies)(transaction);
|
|
268
338
|
});
|
|
269
339
|
}
|
|
270
340
|
function selectComment(id) {
|
|
271
341
|
return (state, dispatch) => {
|
|
272
|
-
const comment =
|
|
342
|
+
const comment = commentById(state, id);
|
|
273
343
|
if (!comment) return false;
|
|
274
344
|
dispatch?.(
|
|
275
345
|
state.tr.setSelection(
|
|
@@ -286,6 +356,7 @@ export {
|
|
|
286
356
|
removeComment,
|
|
287
357
|
removeCommentReply,
|
|
288
358
|
selectComment,
|
|
359
|
+
setCommentBody,
|
|
289
360
|
setCommentResolved,
|
|
290
361
|
updateComment,
|
|
291
362
|
updateCommentReply
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Public comment values and shared attribute readers. */
|
|
2
|
+
import type { CommentReplyData } from "../../../docx/comments";
|
|
3
|
+
/** Who a comment is written by: the name it is shown under, and the identity behind it. */
|
|
2
4
|
export interface CommentAuthor {
|
|
3
5
|
/**
|
|
4
6
|
* The identity behind the name, an opaque string the host application chooses. Recorded in the
|
|
@@ -8,6 +10,14 @@ export interface CommentAuthor {
|
|
|
8
10
|
name: string;
|
|
9
11
|
initials?: string;
|
|
10
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A stretch of text a comment is written for: a pair of document positions, the way a comment
|
|
15
|
+
* already in the document reports its own (`DocumentComment.from`, `.to`).
|
|
16
|
+
*/
|
|
17
|
+
export interface CommentRange {
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
}
|
|
11
21
|
export interface NewComment {
|
|
12
22
|
text: string;
|
|
13
23
|
author: string;
|
|
@@ -17,24 +27,31 @@ export interface NewComment {
|
|
|
17
27
|
/** ISO 8601 timestamp. The current time is used when omitted. */
|
|
18
28
|
date?: string;
|
|
19
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* One comment as a reader of the document sees it. Every field is readonly: the record is the
|
|
32
|
+
* editor's own, worked out once per edit and handed to whoever asks (`editor/plugins/documentProjection`).
|
|
33
|
+
*/
|
|
20
34
|
export interface DocumentComment {
|
|
21
|
-
id: string;
|
|
22
|
-
author: string | null;
|
|
23
|
-
authorId: string | null;
|
|
24
|
-
initials: string | null;
|
|
25
|
-
date: string | null;
|
|
26
|
-
text
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly author: string | null;
|
|
37
|
+
readonly authorId: string | null;
|
|
38
|
+
readonly initials: string | null;
|
|
39
|
+
readonly date: string | null;
|
|
40
|
+
/** What it says, as plain text. The formatting behind it is `setCommentBody`'s to write */
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly from: number;
|
|
43
|
+
readonly to: number;
|
|
44
|
+
readonly referencePos: number;
|
|
45
|
+
readonly resolved: boolean;
|
|
46
|
+
readonly replies: readonly DocumentCommentReply[];
|
|
32
47
|
}
|
|
33
48
|
export interface DocumentCommentReply {
|
|
34
|
-
id: string;
|
|
35
|
-
author: string | null;
|
|
36
|
-
authorId: string | null;
|
|
37
|
-
initials: string | null;
|
|
38
|
-
date: string | null;
|
|
39
|
-
text: string;
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly author: string | null;
|
|
51
|
+
readonly authorId: string | null;
|
|
52
|
+
readonly initials: string | null;
|
|
53
|
+
readonly date: string | null;
|
|
54
|
+
readonly text: string;
|
|
40
55
|
}
|
|
56
|
+
export declare function stringAttr(value: unknown): string | null;
|
|
57
|
+
export declare function repliesAttr(value: unknown): readonly CommentReplyData[];
|