@portone/docx-editor 0.5.1 → 0.6.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 +23 -0
- package/CONTRIBUTING.md +8 -2
- package/README.md +1 -1
- package/dist/DocxEditor.js +3 -3
- package/dist/editor/commands/comments/editing.d.ts +4 -1
- package/dist/editor/commands/comments/editing.js +1 -3
- package/dist/editor/commands/comments/model.d.ts +7 -0
- package/dist/editor/createEditor.js +4 -0
- package/dist/editor/plugins/commentDecorations.js +5 -4
- package/dist/editor/plugins/commentRestoration.d.ts +20 -0
- package/dist/editor/plugins/commentRestoration.js +168 -0
- package/dist/styles/classNames.d.ts +2 -0
- package/dist/styles/classNames.js +2 -0
- package/dist/styles.css +7 -4
- package/dist/ui/CommentsPanel.d.ts +6 -1
- package/dist/ui/CommentsPanel.js +26 -8
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @portone/docx-editor
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#131](https://github.com/portone-io/docx-editor/pull/131) [`5dd468a`](https://github.com/portone-io/docx-editor/commit/5dd468ae14d6210070ab6c8275d16e03b20cf64c) Thanks [@Deea222](https://github.com/Deea222)! - A comment now outlives the text it was written for, and a resolved one stops marking the page.
|
|
8
|
+
|
|
9
|
+
Deleting commented text used to delete the comment and everything said under it. The thread now
|
|
10
|
+
stays. Trim the highlighted text - type over part of it, backspace through its edge - and the
|
|
11
|
+
comment follows what is left. Delete all of it and the comment comes back detached: no longer drawn
|
|
12
|
+
beside the page, since there is nothing to draw it next to, and listed under All comments marked
|
|
13
|
+
"Original content deleted". Cutting commented text leaves it behind the same way instead of dropping
|
|
14
|
+
it. Word has no detached state, so such a comment reaches the saved file as an ordinary comment
|
|
15
|
+
sitting where the deleted text was. Deleting a comment from its own Delete button still deletes it,
|
|
16
|
+
and undo and redo still take a deletion back and make it again.
|
|
17
|
+
|
|
18
|
+
Resolving a thread now takes its highlight off the text, and reopening puts it back. Clicking a
|
|
19
|
+
resolved comment's card in All comments selects the text it was written about and scrolls it into
|
|
20
|
+
view, which `selectComment` now does for every caller.
|
|
21
|
+
|
|
22
|
+
`documentComments` reports a new `anchored` field on every comment, `false` for one that no longer
|
|
23
|
+
marks a stretch of the document. Its existing fields are unchanged, so a panel of your own keeps
|
|
24
|
+
working and can read the new field to decide what a click on a card should do.
|
|
25
|
+
|
|
3
26
|
## 0.5.1
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/CONTRIBUTING.md
CHANGED
|
@@ -44,22 +44,28 @@ Run a specialized check when your change affects its area:
|
|
|
44
44
|
pnpm test:package
|
|
45
45
|
pnpm verify:package
|
|
46
46
|
pnpm test:e2e
|
|
47
|
+
pnpm bench
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
A pull request that changes performance-sensitive code attaches its `pnpm bench` output to the description.
|
|
51
|
+
|
|
49
52
|
[Testing](https://github.com/portone-io/docx-editor/blob/main/docs/testing.md) explains the scope and prerequisites of every check.
|
|
53
|
+
CI runs `pnpm test:package` on every pull request in a separate job; it is not part of the local `pnpm check` command.
|
|
50
54
|
|
|
51
55
|
A command added to `./commands` or `./table` needs a writer probe in `src/docx/__testing__/writerProbes.ts` with a `check` of its immediate effect, so its export is validated before another command can overwrite it, or an entry in `NOT_A_WRITER` giving the reason it reaches no writer.
|
|
52
56
|
|
|
53
57
|
## Changesets
|
|
54
58
|
|
|
55
|
-
If you changed
|
|
59
|
+
If you changed production code under `src/`, run `pnpm changeset` and commit the file it writes.
|
|
56
60
|
It becomes the CHANGELOG entry, so write it for someone reading release notes rather than the diff.
|
|
61
|
+
Changes limited to tests, fixtures, scripts, or documentation do not need a changeset.
|
|
57
62
|
|
|
58
|
-
The public API is the exported names and types of the four entry points - `.`, `./core`, `./commands`, and `./table` - together with the parts of the document model that [What a plugin may rely on](https://docx-editor.portone.io/docs/
|
|
63
|
+
The public API is the exported names and types of the four entry points - `.`, `./core`, `./commands`, and `./table` - together with the parts of the document model that [What a plugin may rely on](https://docx-editor.portone.io/docs/editor-api/plugins-and-presets#what-a-plugin-may-rely-on) calls stable.
|
|
59
64
|
An attr that page calls internal is not part of it, and neither is anything under `src/` that no entry point reaches.
|
|
60
65
|
|
|
61
66
|
Choose `patch` unless the change adds to that surface, takes something out of it, or changes what a part of it means.
|
|
62
67
|
Those are `minor` while the package is below 1.0.
|
|
68
|
+
A diff to `src/schema/attrRoles.ts` is where to check this: a change to a stable document-model attr is `minor` even when the declaration reports do not change.
|
|
63
69
|
A declaration added, removed, or rewritten in `etc/*.api.md` after running `pnpm api:update` is what that looks like; a line that only gains or loses an `(undocumented)` marker is not.
|
|
64
70
|
Commit the regenerated report in the same commit as the changeset.
|
|
65
71
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ export function Editor({
|
|
|
47
47
|
- Preserves document structures and package parts across edits, and records who wrote each comment.
|
|
48
48
|
- Reads document styles, theme fonts, page layout, and CJK font information for display.
|
|
49
49
|
|
|
50
|
-
See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build [
|
|
50
|
+
See [Feature support](https://docx-editor.portone.io/docs/features) for the support matrix. Build controls of your own with the [Editor API](https://docx-editor.portone.io/docs/editor-api) or use [programmatic DOCX import and export](https://docx-editor.portone.io/docs/core).
|
|
51
51
|
|
|
52
52
|
The full [documentation](https://docx-editor.portone.io/docs) lives on the documentation site.
|
|
53
53
|
|
package/dist/DocxEditor.js
CHANGED
|
@@ -31,7 +31,7 @@ import { usePageLayout } from "./page/usePageLayout.js";
|
|
|
31
31
|
import { editingProtection, protectionOf } from "./schema/protectionState.js";
|
|
32
32
|
import { storyNodeOf } from "./schema/stories.js";
|
|
33
33
|
import { editorClassNames } from "./styles/classNames.js";
|
|
34
|
-
import { CommentsPanel } from "./ui/CommentsPanel.js";
|
|
34
|
+
import { CommentsPanel, shownBesideThePage } from "./ui/CommentsPanel.js";
|
|
35
35
|
import { LinkCard } from "./ui/LinkCard.js";
|
|
36
36
|
import { LinkPanel } from "./ui/LinkPanel.js";
|
|
37
37
|
import { NotesPanel } from "./ui/NotesPanel.js";
|
|
@@ -280,9 +280,9 @@ function DocxEditorSurface({
|
|
|
280
280
|
const tableAnchor = live && !textAnchor ? tableMenuAnchor(live.state) : null;
|
|
281
281
|
const linkAtCursor = live && !live.view.composing && !isLinkPanelOpen(live.state) ? activeLinkSpan(live.state) : null;
|
|
282
282
|
const comments = live?.state === void 0 ? [] : commentProjection.read(live.state).comments;
|
|
283
|
-
const
|
|
283
|
+
const hasRailComments = comments.some(shownBesideThePage);
|
|
284
284
|
const composerOpen = live !== null && isCommentComposerOpen(live.state);
|
|
285
|
-
const showComments = live !== null && (commentsOpen || composerOpen ||
|
|
285
|
+
const showComments = live !== null && (commentsOpen || composerOpen || hasRailComments);
|
|
286
286
|
const commentsPanel = live && showComments && /* @__PURE__ */ jsx(
|
|
287
287
|
CommentsPanel,
|
|
288
288
|
{
|
|
@@ -41,5 +41,8 @@ export declare function removeCommentReply(commentId: string, replyId: string):
|
|
|
41
41
|
* document without standing for anything.
|
|
42
42
|
*/
|
|
43
43
|
export declare function removeComment(id: string): Command;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Selects the text a comment is anchored to and scrolls it into view. A comment marking no stretch
|
|
46
|
+
* - one written at a point, or one whose text was deleted - takes the caret to where it stands.
|
|
47
|
+
*/
|
|
45
48
|
export declare function selectComment(id: string): Command;
|
|
@@ -342,9 +342,7 @@ function selectComment(id) {
|
|
|
342
342
|
const comment = commentById(state, id);
|
|
343
343
|
if (!comment) return false;
|
|
344
344
|
dispatch?.(
|
|
345
|
-
state.tr.setSelection(
|
|
346
|
-
TextSelection.create(state.doc, comment.from, comment.to)
|
|
347
|
-
)
|
|
345
|
+
state.tr.setSelection(TextSelection.create(state.doc, comment.from, comment.to)).scrollIntoView()
|
|
348
346
|
);
|
|
349
347
|
return true;
|
|
350
348
|
};
|
|
@@ -42,6 +42,13 @@ export interface DocumentComment {
|
|
|
42
42
|
readonly from: number;
|
|
43
43
|
readonly to: number;
|
|
44
44
|
readonly referencePos: number;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the comment still marks a stretch of the document: both range markers stand, and the
|
|
47
|
+
* opening one stands before the closing one. A comment that lost both markers with the text an
|
|
48
|
+
* edit deleted is detached - `from` and `to` are then both where its reference stands - and is
|
|
49
|
+
* drawn nowhere on the page.
|
|
50
|
+
*/
|
|
51
|
+
readonly anchored: boolean;
|
|
45
52
|
readonly resolved: boolean;
|
|
46
53
|
readonly replies: readonly DocumentCommentReply[];
|
|
47
54
|
}
|
|
@@ -26,6 +26,7 @@ import { imageFiles } from "./imageFiles.js";
|
|
|
26
26
|
import { columnResize } from "./plugins/columnResize.js";
|
|
27
27
|
import { commentComposer } from "./plugins/commentComposer.js";
|
|
28
28
|
import { commentDecorations } from "./plugins/commentDecorations.js";
|
|
29
|
+
import { commentRestoration } from "./plugins/commentRestoration.js";
|
|
29
30
|
import {
|
|
30
31
|
displayDerivation,
|
|
31
32
|
withDerivedDisplay
|
|
@@ -96,6 +97,9 @@ function createEditorState(doc, options = {}) {
|
|
|
96
97
|
// The list beside the page, the lookup the comment commands ask, and these ranges are the
|
|
97
98
|
// one walk this plugin holds (`plugins/commentDecorations`)
|
|
98
99
|
commentDecorations(),
|
|
100
|
+
// A comment outlives the text it was written for: an edit that sweeps its reference away has
|
|
101
|
+
// it put back where the deletion left, detached from the page (`plugins/commentRestoration`)
|
|
102
|
+
commentRestoration(),
|
|
99
103
|
// What the notes under the page are, worked out from the document the same way
|
|
100
104
|
noteProjection.plugin,
|
|
101
105
|
// Adjacent text tabs still need separate DOM ranges for layout and pointer selection.
|
|
@@ -34,7 +34,7 @@ function commentsIn(doc) {
|
|
|
34
34
|
const id = stringAttr(node.attrs.id) ?? "";
|
|
35
35
|
const start = markers.starts.get(id);
|
|
36
36
|
const end = markers.ends.get(id);
|
|
37
|
-
const
|
|
37
|
+
const anchored = start !== void 0 && end !== void 0 && start < end;
|
|
38
38
|
const point = start ?? end ?? pos;
|
|
39
39
|
return {
|
|
40
40
|
id,
|
|
@@ -43,9 +43,10 @@ function commentsIn(doc) {
|
|
|
43
43
|
initials: stringAttr(node.attrs.initials),
|
|
44
44
|
date: stringAttr(node.attrs.date),
|
|
45
45
|
text: bodyText(doc, id),
|
|
46
|
-
from:
|
|
47
|
-
to:
|
|
46
|
+
from: anchored ? start + 1 : point,
|
|
47
|
+
to: anchored ? end : point,
|
|
48
48
|
referencePos: pos,
|
|
49
|
+
anchored,
|
|
49
50
|
resolved: node.attrs.resolved === true,
|
|
50
51
|
replies: repliesAttr(node.attrs.replies).map((reply) => ({
|
|
51
52
|
id: reply.id,
|
|
@@ -69,7 +70,7 @@ function deriveComments(doc) {
|
|
|
69
70
|
byId,
|
|
70
71
|
decorations: DecorationSet.create(
|
|
71
72
|
doc,
|
|
72
|
-
comments.filter((comment) => comment.from < comment.to).map(
|
|
73
|
+
comments.filter((comment) => !comment.resolved && comment.from < comment.to).map(
|
|
73
74
|
(comment) => Decoration.inline(comment.from, comment.to, {
|
|
74
75
|
class: editorClassNames.commentRange,
|
|
75
76
|
"data-comment-id": comment.id
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a comment when the text it was written for goes away.
|
|
3
|
+
*
|
|
4
|
+
* Deleting commented text is an edit of the body, not a decision about the thread, so a body edit
|
|
5
|
+
* that takes a comment's nodes away has them put back at the spots the edit left: the thread stays
|
|
6
|
+
* anchored where one range marker survived, detached where both went (`DocumentComment.anchored`).
|
|
7
|
+
*
|
|
8
|
+
* The comment's story (`docx/story`) tells such an edit from `removeComment`: a body deletion does
|
|
9
|
+
* not reach the story, so a comment is put back while its story stands, which only `removeComment`
|
|
10
|
+
* takes away. The cost is that a reference the comments part held no body for is never put back.
|
|
11
|
+
*
|
|
12
|
+
* A restoration is an ordinary transaction the guards judge (`editor/plugins/lockedContent`): the
|
|
13
|
+
* nodes take the nearest spot left open, and a document leaving none loses the comment. A range
|
|
14
|
+
* marker with no home takes its surviving counterpart down, so no export carries half a range.
|
|
15
|
+
*
|
|
16
|
+
* A restoration needs no deferral around an open IME composition: the nodes go in beside the
|
|
17
|
+
* composed text rather than rewriting it (`e2e/hangulComposition.spec.ts`).
|
|
18
|
+
*/
|
|
19
|
+
import { Plugin } from "prosemirror-state";
|
|
20
|
+
export declare function commentRestoration(): Plugin;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/editor/plugins/commentRestoration.ts
|
|
2
|
+
import {
|
|
3
|
+
Plugin,
|
|
4
|
+
TextSelection
|
|
5
|
+
} from "prosemirror-state";
|
|
6
|
+
import { storyKey, storyOf } from "../../docx/story.js";
|
|
7
|
+
import { docxSchema } from "../../schema/index.js";
|
|
8
|
+
import { editShut, transactionTouchesComments } from "../../schema/guards.js";
|
|
9
|
+
import { stringAttr } from "../commands/comments/model.js";
|
|
10
|
+
var NODE_FIELD = {
|
|
11
|
+
commentStart: "start",
|
|
12
|
+
commentEnd: "end",
|
|
13
|
+
commentReference: "reference"
|
|
14
|
+
};
|
|
15
|
+
var NODE_FIELDS = ["start", "end", "reference"];
|
|
16
|
+
var MAPPING_SIDE = {
|
|
17
|
+
start: 1,
|
|
18
|
+
end: -1,
|
|
19
|
+
reference: -1
|
|
20
|
+
};
|
|
21
|
+
var NO_NODES = { start: null, end: null, reference: null };
|
|
22
|
+
function commentNodesIn(doc) {
|
|
23
|
+
const found = /* @__PURE__ */ new Map();
|
|
24
|
+
doc.descendants((node, pos) => {
|
|
25
|
+
const field = NODE_FIELD[node.type.name];
|
|
26
|
+
if (field === void 0) return true;
|
|
27
|
+
const id = stringAttr(node.attrs.id);
|
|
28
|
+
if (id === null) return true;
|
|
29
|
+
const nodes = found.get(id) ?? { ...NO_NODES };
|
|
30
|
+
found.set(id, nodes);
|
|
31
|
+
if (nodes[field] === null) nodes[field] = { node, pos };
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
function inlineRanges(doc) {
|
|
37
|
+
const type = docxSchema.nodes.commentReference;
|
|
38
|
+
const ranges = [];
|
|
39
|
+
doc.descendants((node, pos) => {
|
|
40
|
+
if (!node.isTextblock) return true;
|
|
41
|
+
if (node.type.contentMatch.matchType(type) !== null) {
|
|
42
|
+
ranges.push({ from: pos + 1, to: pos + 1 + node.content.size });
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
});
|
|
46
|
+
return ranges;
|
|
47
|
+
}
|
|
48
|
+
function within(ranges, pos) {
|
|
49
|
+
return ranges.some((range) => pos >= range.from && pos <= range.to);
|
|
50
|
+
}
|
|
51
|
+
function* homes(ranges, wanted, caret) {
|
|
52
|
+
if (within(ranges, wanted)) yield wanted;
|
|
53
|
+
if (caret !== wanted && within(ranges, caret)) yield caret;
|
|
54
|
+
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
const range = ranges[index];
|
|
56
|
+
if (range !== void 0 && range.to < wanted) yield range.to;
|
|
57
|
+
}
|
|
58
|
+
for (const range of ranges) {
|
|
59
|
+
if (range.from > wanted) yield range.from;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function allowedHome(state, ranges, wanted, caret) {
|
|
63
|
+
for (const at of homes(ranges, wanted, caret)) {
|
|
64
|
+
if (!editShut(state, { kind: "insert", at })) return at;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function mappedThrough(transactions, pos, side) {
|
|
69
|
+
return transactions.reduce((at, tr) => tr.mapping.map(at, side), pos);
|
|
70
|
+
}
|
|
71
|
+
function restorable(node) {
|
|
72
|
+
return node.type.create(
|
|
73
|
+
node.attrs,
|
|
74
|
+
null,
|
|
75
|
+
node.marks.filter((mark) => mark.type === docxSchema.marks.run)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function planRestoration(transactions, oldState, newState) {
|
|
79
|
+
const before = commentNodesIn(oldState.doc);
|
|
80
|
+
const after = commentNodesIn(newState.doc);
|
|
81
|
+
const ranges = inlineRanges(newState.doc);
|
|
82
|
+
const caret = newState.selection.from;
|
|
83
|
+
const plan = { insertions: [], removals: [] };
|
|
84
|
+
for (const [id, was] of before) {
|
|
85
|
+
if (storyOf(newState.doc, storyKey("comment", id)) === null) continue;
|
|
86
|
+
const now = after.get(id) ?? NO_NODES;
|
|
87
|
+
for (const field of NODE_FIELDS) {
|
|
88
|
+
const lost = was[field];
|
|
89
|
+
if (lost === null || now[field] !== null) continue;
|
|
90
|
+
const counterpart = field === "start" ? now.end : field === "end" ? now.start : null;
|
|
91
|
+
if (field !== "reference" && counterpart === null) continue;
|
|
92
|
+
const at = allowedHome(
|
|
93
|
+
newState,
|
|
94
|
+
ranges,
|
|
95
|
+
mappedThrough(transactions, lost.pos, MAPPING_SIDE[field]),
|
|
96
|
+
caret
|
|
97
|
+
);
|
|
98
|
+
if (at !== null) {
|
|
99
|
+
plan.insertions.push({
|
|
100
|
+
node: restorable(lost.node),
|
|
101
|
+
at,
|
|
102
|
+
was: lost.pos
|
|
103
|
+
});
|
|
104
|
+
} else if (counterpart !== null && !markerShut(newState, counterpart)) {
|
|
105
|
+
plan.removals.push(counterpart);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return plan;
|
|
110
|
+
}
|
|
111
|
+
function markerShut(state, marker) {
|
|
112
|
+
return editShut(state, {
|
|
113
|
+
kind: "replace",
|
|
114
|
+
from: marker.pos,
|
|
115
|
+
to: marker.pos + marker.node.nodeSize
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function pastCommentNodes(doc, pos, direction) {
|
|
119
|
+
let at = pos;
|
|
120
|
+
for (; ; ) {
|
|
121
|
+
const resolved = doc.resolve(at);
|
|
122
|
+
const next = direction < 0 ? resolved.nodeBefore : resolved.nodeAfter;
|
|
123
|
+
if (next === null || NODE_FIELD[next.type.name] === void 0) return at;
|
|
124
|
+
at += direction * next.nodeSize;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function withCaretPastMarkers(tr, oldState, newState) {
|
|
128
|
+
const { selection } = newState;
|
|
129
|
+
if (!(selection instanceof TextSelection) || !selection.empty) return tr;
|
|
130
|
+
if (!tr.doc.eq(oldState.doc)) return tr;
|
|
131
|
+
const direction = selection.from < oldState.selection.from ? -1 : 1;
|
|
132
|
+
return tr.setSelection(
|
|
133
|
+
TextSelection.create(
|
|
134
|
+
tr.doc,
|
|
135
|
+
pastCommentNodes(tr.doc, tr.selection.from, direction)
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function restoration(transactions, oldState, newState) {
|
|
140
|
+
const plan = planRestoration(transactions, oldState, newState);
|
|
141
|
+
if (plan.insertions.length === 0 && plan.removals.length === 0) return null;
|
|
142
|
+
const tr = newState.tr;
|
|
143
|
+
for (const marker of [...plan.removals].sort(
|
|
144
|
+
(left, right) => right.pos - left.pos
|
|
145
|
+
)) {
|
|
146
|
+
tr.delete(marker.pos, marker.pos + marker.node.nodeSize);
|
|
147
|
+
}
|
|
148
|
+
const ordered = [...plan.insertions].sort(
|
|
149
|
+
(left, right) => left.at - right.at || left.was - right.was
|
|
150
|
+
);
|
|
151
|
+
for (const { node, at } of ordered) {
|
|
152
|
+
tr.insert(tr.mapping.map(at), node);
|
|
153
|
+
}
|
|
154
|
+
if (!tr.docChanged) return null;
|
|
155
|
+
return withCaretPastMarkers(tr, oldState, newState);
|
|
156
|
+
}
|
|
157
|
+
function commentRestoration() {
|
|
158
|
+
return new Plugin({
|
|
159
|
+
appendTransaction(transactions, oldState, newState) {
|
|
160
|
+
if (!transactions.some((tr) => tr.docChanged)) return null;
|
|
161
|
+
if (!transactions.some(transactionTouchesComments)) return null;
|
|
162
|
+
return restoration(transactions, oldState, newState);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
commentRestoration
|
|
168
|
+
};
|
|
@@ -140,6 +140,8 @@ export declare const editorClassNames: {
|
|
|
140
140
|
readonly commentMeta: "docx-editor-comment-meta";
|
|
141
141
|
readonly commentAuthor: "docx-editor-comment-author";
|
|
142
142
|
readonly commentDate: "docx-editor-comment-date";
|
|
143
|
+
/** The note on a card whose comment lost the text it was written for */
|
|
144
|
+
readonly commentDetached: "docx-editor-comment-detached";
|
|
143
145
|
readonly commentBody: "docx-editor-comment-body";
|
|
144
146
|
readonly commentActions: "docx-editor-comment-actions";
|
|
145
147
|
readonly commentPrimaryAction: "docx-editor-comment-primary-action";
|
|
@@ -136,6 +136,8 @@ var editorClassNames = {
|
|
|
136
136
|
commentMeta: `${PREFIX}-comment-meta`,
|
|
137
137
|
commentAuthor: `${PREFIX}-comment-author`,
|
|
138
138
|
commentDate: `${PREFIX}-comment-date`,
|
|
139
|
+
/** The note on a card whose comment lost the text it was written for */
|
|
140
|
+
commentDetached: `${PREFIX}-comment-detached`,
|
|
139
141
|
commentBody: `${PREFIX}-comment-body`,
|
|
140
142
|
commentActions: `${PREFIX}-comment-actions`,
|
|
141
143
|
commentPrimaryAction: `${PREFIX}-comment-primary-action`,
|
package/dist/styles.css
CHANGED
|
@@ -270,12 +270,15 @@
|
|
|
270
270
|
margin-top: 2px;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
.docx-editor-comment-detached {
|
|
274
|
+
display: block;
|
|
275
|
+
margin-top: 2px;
|
|
276
|
+
color: #8a8a8a;
|
|
277
|
+
font-style: italic;
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
button.docx-editor-comment-meta
|
|
278
|
-
cursor:
|
|
280
|
+
button.docx-editor-comment-meta {
|
|
281
|
+
cursor: pointer;
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
.docx-editor-comment-body {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { EditorState } from "prosemirror-state";
|
|
2
2
|
import type { EditorView } from "prosemirror-view";
|
|
3
3
|
import { type ReactElement } from "react";
|
|
4
|
-
import { type CommentAuthor } from "../editor/commands/commentCommands";
|
|
4
|
+
import { type CommentAuthor, type DocumentComment } from "../editor/commands/commentCommands";
|
|
5
|
+
/**
|
|
6
|
+
* Whether the rail beside the page carries this comment: the thread is unsettled, and the text it
|
|
7
|
+
* was written for is still there to draw it next to.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shownBesideThePage(comment: DocumentComment): boolean;
|
|
5
10
|
export interface CommentsPanelProps {
|
|
6
11
|
view: EditorView;
|
|
7
12
|
state: EditorState;
|
package/dist/ui/CommentsPanel.js
CHANGED
|
@@ -43,6 +43,19 @@ function run(view, command) {
|
|
|
43
43
|
}
|
|
44
44
|
var REFUSED_COMMENT = "This comment could not be added. The text it was written for may have been changed or locked since.";
|
|
45
45
|
var REFUSED_REPLY = "This reply could not be added. The comment it answers may have been changed or deleted since.";
|
|
46
|
+
var DETACHED_LABEL = "Original content deleted";
|
|
47
|
+
function shownBesideThePage(comment) {
|
|
48
|
+
return !comment.resolved && comment.anchored;
|
|
49
|
+
}
|
|
50
|
+
function CommentIdentity({
|
|
51
|
+
comment
|
|
52
|
+
}) {
|
|
53
|
+
const date = shownDate(comment.date);
|
|
54
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
55
|
+
/* @__PURE__ */ jsx("span", { className: editorClassNames.commentAuthor, children: comment.author ?? "Unknown author" }),
|
|
56
|
+
date && /* @__PURE__ */ jsx("span", { className: editorClassNames.commentDate, children: date })
|
|
57
|
+
] });
|
|
58
|
+
}
|
|
46
59
|
function sameTarget(left, right) {
|
|
47
60
|
return left?.commentId === right.commentId && left.replyId === right.replyId;
|
|
48
61
|
}
|
|
@@ -74,7 +87,7 @@ function CommentsPanel({
|
|
|
74
87
|
if (Number.isNaN(leftTime)) return 1;
|
|
75
88
|
if (Number.isNaN(rightTime)) return -1;
|
|
76
89
|
return rightTime - leftTime;
|
|
77
|
-
}) : comments.filter(
|
|
90
|
+
}) : comments.filter(shownBesideThePage),
|
|
78
91
|
[allCommentsOpen, comments]
|
|
79
92
|
);
|
|
80
93
|
const { positions, canvasHeight } = useCommentRailLayout({
|
|
@@ -158,22 +171,26 @@ function CommentsPanel({
|
|
|
158
171
|
{
|
|
159
172
|
className: editorClassNames.commentPosition,
|
|
160
173
|
"data-resolved": comment.resolved ? "true" : void 0,
|
|
174
|
+
"data-detached": comment.anchored ? void 0 : "true",
|
|
161
175
|
"data-comment-position": comment.id,
|
|
162
176
|
style: allCommentsOpen ? void 0 : { top: `${positions.get(comment.id) ?? 64}px` },
|
|
163
177
|
children: /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentCard, children: [
|
|
164
178
|
/* @__PURE__ */ jsxs("div", { className: editorClassNames.commentHeader, children: [
|
|
165
|
-
/* @__PURE__ */
|
|
179
|
+
comment.anchored ? /* @__PURE__ */ jsx(
|
|
166
180
|
"button",
|
|
167
181
|
{
|
|
168
182
|
type: "button",
|
|
169
183
|
className: editorClassNames.commentMeta,
|
|
170
184
|
onClick: () => run(view, selectComment(comment.id)),
|
|
171
|
-
|
|
172
|
-
children: [
|
|
173
|
-
/* @__PURE__ */ jsx("span", { className: editorClassNames.commentAuthor, children: comment.author ?? "Unknown author" }),
|
|
174
|
-
shownDate(comment.date) && /* @__PURE__ */ jsx("span", { className: editorClassNames.commentDate, children: shownDate(comment.date) })
|
|
175
|
-
]
|
|
185
|
+
children: /* @__PURE__ */ jsx(CommentIdentity, { comment })
|
|
176
186
|
}
|
|
187
|
+
) : (
|
|
188
|
+
// Nothing on the page to go to, so the identity is text rather than a button:
|
|
189
|
+
// a click here leaves the caret and the scroll where the reader left them
|
|
190
|
+
/* @__PURE__ */ jsxs("div", { className: editorClassNames.commentMeta, children: [
|
|
191
|
+
/* @__PURE__ */ jsx(CommentIdentity, { comment }),
|
|
192
|
+
/* @__PURE__ */ jsx("span", { className: editorClassNames.commentDetached, children: DETACHED_LABEL })
|
|
193
|
+
] })
|
|
177
194
|
),
|
|
178
195
|
writer !== null && !rootEditing && /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentIconActions, children: [
|
|
179
196
|
rootOwned && !comment.resolved && /* @__PURE__ */ jsx(
|
|
@@ -361,5 +378,6 @@ function CommentsPanel({
|
|
|
361
378
|
);
|
|
362
379
|
}
|
|
363
380
|
export {
|
|
364
|
-
CommentsPanel
|
|
381
|
+
CommentsPanel,
|
|
382
|
+
shownBesideThePage
|
|
365
383
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portone/docx-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A simple DOCX editor for React, built directly on OOXML.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"build:demo": "vite build --config demo/vite.config.ts",
|
|
108
108
|
"dev:site": "pnpm --filter @portone/docx-editor-site dev",
|
|
109
109
|
"build:site": "pnpm --filter @portone/docx-editor-site build",
|
|
110
|
+
"bench": "vitest run --config vitest.bench.config.ts --disableConsoleIntercept",
|
|
110
111
|
"check": "pnpm lint && pnpm typecheck && pnpm test && pnpm test:fixtures && pnpm test:site-release && pnpm test:release && pnpm check:demo-library",
|
|
111
112
|
"typecheck": "tsc --noEmit && tsc --noEmit -p e2e && tsc --noEmit -p demo && pnpm --filter @portone/docx-editor-site typecheck",
|
|
112
113
|
"lint": "pnpm biome check . --diagnostic-level=error",
|