@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.
Files changed (180) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/CONTRIBUTING.md +2 -0
  3. package/assets/editor.png +0 -0
  4. package/dist/DocxEditor.d.ts +1 -1
  5. package/dist/DocxEditor.js +44 -25
  6. package/dist/docx/cloning.js +4 -9
  7. package/dist/docx/commentOnlyChange.d.ts +5 -3
  8. package/dist/docx/comments/grammar.d.ts +27 -10
  9. package/dist/docx/comments/grammar.js +18 -67
  10. package/dist/docx/comments/model.d.ts +5 -6
  11. package/dist/docx/comments/model.js +0 -9
  12. package/dist/docx/comments/parts.d.ts +16 -10
  13. package/dist/docx/comments/parts.js +65 -13
  14. package/dist/docx/comments/people.d.ts +11 -3
  15. package/dist/docx/comments/people.js +20 -6
  16. package/dist/docx/comments/policy.js +7 -1
  17. package/dist/docx/comments/reading.d.ts +4 -2
  18. package/dist/docx/comments/reading.js +1 -14
  19. package/dist/docx/comments/writing.js +65 -28
  20. package/dist/docx/exportDocx.js +10 -14
  21. package/dist/docx/exportRefs.d.ts +8 -3
  22. package/dist/docx/exportRefs.js +2 -1
  23. package/dist/docx/fidelity.js +24 -10
  24. package/dist/docx/fields.d.ts +42 -0
  25. package/dist/docx/fields.js +76 -0
  26. package/dist/docx/formatting/styles.d.ts +0 -2
  27. package/dist/docx/formatting/styles.js +2 -15
  28. package/dist/docx/headersFooters.d.ts +89 -20
  29. package/dist/docx/headersFooters.js +163 -172
  30. package/dist/docx/identities.js +17 -26
  31. package/dist/docx/importDocx.d.ts +1 -1
  32. package/dist/docx/importDocx.js +100 -48
  33. package/dist/docx/importParagraph.d.ts +18 -5
  34. package/dist/docx/importParagraph.js +75 -128
  35. package/dist/docx/importPolicy.d.ts +70 -0
  36. package/dist/docx/importPolicy.js +195 -0
  37. package/dist/docx/importPreserved.d.ts +30 -0
  38. package/dist/docx/importPreserved.js +54 -0
  39. package/dist/docx/importTable.js +56 -12
  40. package/dist/docx/invariants.js +16 -18
  41. package/dist/docx/notes.d.ts +6 -1
  42. package/dist/docx/notes.js +6 -16
  43. package/dist/docx/pageGeometry.d.ts +5 -14
  44. package/dist/docx/pageGeometry.js +0 -12
  45. package/dist/docx/protectionPolicy.d.ts +13 -5
  46. package/dist/docx/protectionPolicy.js +32 -22
  47. package/dist/docx/scan.d.ts +18 -8
  48. package/dist/docx/scan.js +17 -11
  49. package/dist/docx/sections.d.ts +126 -0
  50. package/dist/docx/sections.js +207 -0
  51. package/dist/docx/serializeBlock.d.ts +13 -4
  52. package/dist/docx/serializeBlock.js +16 -18
  53. package/dist/docx/serializeParagraph.d.ts +9 -4
  54. package/dist/docx/serializeParagraph.js +23 -58
  55. package/dist/docx/serializePreserved.d.ts +14 -0
  56. package/dist/docx/serializePreserved.js +24 -0
  57. package/dist/docx/serializeStory.d.ts +17 -0
  58. package/dist/docx/serializeStory.js +17 -0
  59. package/dist/docx/serializeTable.js +10 -13
  60. package/dist/docx/session.d.ts +23 -6
  61. package/dist/docx/session.js +26 -5
  62. package/dist/docx/story.d.ts +140 -0
  63. package/dist/docx/story.js +237 -0
  64. package/dist/docx/storyProjection.js +6 -1
  65. package/dist/docx/tableTemplate.d.ts +25 -0
  66. package/dist/docx/tableTemplate.js +30 -16
  67. package/dist/docx/wrappers.d.ts +67 -0
  68. package/dist/docx/wrappers.js +117 -0
  69. package/dist/editor/clipboard/blockReaders.d.ts +74 -0
  70. package/dist/editor/clipboard/blockReaders.js +193 -0
  71. package/dist/editor/clipboard/htmlReader.d.ts +26 -0
  72. package/dist/editor/clipboard/htmlReader.js +376 -0
  73. package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
  74. package/dist/editor/clipboard/inlineFormatting.js +1 -7
  75. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  76. package/dist/editor/clipboard/internalChannel.js +58 -0
  77. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  78. package/dist/editor/clipboard/normalizers.js +199 -0
  79. package/dist/editor/clipboard/parser.d.ts +34 -0
  80. package/dist/editor/clipboard/parser.js +58 -0
  81. package/dist/editor/clipboard/plugin.d.ts +21 -0
  82. package/dist/editor/clipboard/plugin.js +188 -0
  83. package/dist/editor/clipboard/readContext.d.ts +34 -0
  84. package/dist/editor/clipboard/readContext.js +25 -0
  85. package/dist/editor/clipboard/readers.d.ts +33 -0
  86. package/dist/editor/clipboard/readers.js +37 -0
  87. package/dist/editor/clipboard/source.d.ts +12 -0
  88. package/dist/editor/clipboard/source.js +21 -0
  89. package/dist/editor/commands/comments/editing.d.ts +26 -6
  90. package/dist/editor/commands/comments/editing.js +177 -106
  91. package/dist/editor/commands/comments/model.d.ts +34 -17
  92. package/dist/editor/commands/comments/model.js +11 -0
  93. package/dist/editor/commands/comments/reading.d.ts +5 -3
  94. package/dist/editor/commands/comments/reading.js +8 -58
  95. package/dist/editor/commands/index.d.ts +2 -2
  96. package/dist/editor/commands/index.js +2 -0
  97. package/dist/editor/commands/linkCommands.js +20 -5
  98. package/dist/editor/commands/listCommands.d.ts +6 -0
  99. package/dist/editor/commands/listCommands.js +3 -2
  100. package/dist/editor/commands/lockCommands.js +59 -16
  101. package/dist/editor/commands/noteQueries.d.ts +22 -5
  102. package/dist/editor/commands/noteQueries.js +34 -6
  103. package/dist/editor/commands/paragraphCommands.js +2 -2
  104. package/dist/editor/createEditor.js +13 -3
  105. package/dist/editor/documentStyles.d.ts +26 -4
  106. package/dist/editor/documentStyles.js +9 -4
  107. package/dist/editor/imageFiles.d.ts +2 -2
  108. package/dist/editor/imageFiles.js +2 -0
  109. package/dist/editor/insertTable.d.ts +1 -1
  110. package/dist/editor/insertTable.js +2 -2
  111. package/dist/editor/plainText.d.ts +12 -0
  112. package/dist/editor/plainText.js +2 -1
  113. package/dist/editor/plugins/columnResize.js +6 -3
  114. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  115. package/dist/editor/plugins/commentComposer.js +46 -0
  116. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  117. package/dist/editor/plugins/commentDecorations.js +89 -15
  118. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  119. package/dist/editor/plugins/documentProjection.js +22 -0
  120. package/dist/editor/plugins/imagePaste.js +15 -8
  121. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  122. package/dist/editor/plugins/linkPanel.js +13 -27
  123. package/dist/editor/plugins/panelState.d.ts +67 -0
  124. package/dist/editor/plugins/panelState.js +54 -0
  125. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  126. package/dist/editor/plugins/tableContextMenu.js +30 -47
  127. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  128. package/dist/editor/plugins/textContextMenu.js +30 -47
  129. package/dist/ooxml/fragment.d.ts +11 -0
  130. package/dist/ooxml/fragment.js +14 -4
  131. package/dist/ooxml/image.d.ts +3 -4
  132. package/dist/ooxml/image.js +50 -5
  133. package/dist/ooxml/names.d.ts +6 -0
  134. package/dist/ooxml/names.js +2 -0
  135. package/dist/ooxml/props.d.ts +10 -0
  136. package/dist/ooxml/props.js +28 -4
  137. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  138. package/dist/ooxml/rangeMarkers.js +24 -0
  139. package/dist/page/PageGuides.d.ts +7 -3
  140. package/dist/page/PageGuides.js +13 -24
  141. package/dist/page/pageLayout.d.ts +64 -5
  142. package/dist/page/pageLayout.js +90 -29
  143. package/dist/page/usePageLayout.d.ts +14 -4
  144. package/dist/page/usePageLayout.js +19 -20
  145. package/dist/schema/attrRoles.js +46 -23
  146. package/dist/schema/clipboard.d.ts +72 -0
  147. package/dist/schema/clipboard.js +288 -0
  148. package/dist/schema/docxSchema.d.ts +9 -1
  149. package/dist/schema/docxSchema.js +217 -165
  150. package/dist/schema/editGuard.d.ts +1 -1
  151. package/dist/schema/guards.js +2 -2
  152. package/dist/schema/index.d.ts +1 -0
  153. package/dist/schema/locks.d.ts +3 -0
  154. package/dist/schema/locks.js +29 -12
  155. package/dist/schema/preservedFragments.d.ts +12 -0
  156. package/dist/schema/preservedFragments.js +38 -0
  157. package/dist/schema/preservedGuards.d.ts +9 -7
  158. package/dist/schema/preservedGuards.js +112 -11
  159. package/dist/schema/protection.d.ts +34 -5
  160. package/dist/schema/protection.js +42 -14
  161. package/dist/schema/rendering.d.ts +14 -0
  162. package/dist/schema/rendering.js +27 -12
  163. package/dist/schema/stories.d.ts +49 -0
  164. package/dist/schema/stories.js +78 -0
  165. package/dist/schema/wrappers.d.ts +58 -0
  166. package/dist/schema/wrappers.js +77 -0
  167. package/dist/styles/classNames.d.ts +3 -5
  168. package/dist/styles/classNames.js +3 -5
  169. package/dist/styles.css +74 -12
  170. package/dist/table/resize.d.ts +5 -3
  171. package/dist/ui/CommentsPanel.d.ts +1 -3
  172. package/dist/ui/CommentsPanel.js +24 -15
  173. package/dist/ui/NotesPanel.js +2 -2
  174. package/dist/ui/TextMenu.d.ts +1 -3
  175. package/dist/ui/TextMenu.js +10 -6
  176. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  177. package/dist/ui/comments/CommentComposer.js +26 -3
  178. package/package.json +5 -4
  179. package/dist/editor/externalClipboard.d.ts +0 -22
  180. package/dist/editor/externalClipboard.js +0 -486
@@ -0,0 +1,78 @@
1
+ // src/schema/stories.ts
2
+ import { sameSource } from "./sourceEquality.js";
3
+ var STORIES_ATTR = "stories";
4
+ var STORY_KINDS = [
5
+ "comment",
6
+ "footnote",
7
+ "endnote",
8
+ "header",
9
+ "footer"
10
+ ];
11
+ function storyKey(kind, id) {
12
+ return `${kind}:${id}`;
13
+ }
14
+ function asStoryKey(text) {
15
+ const at = text.indexOf(":");
16
+ if (at < 1) return null;
17
+ const named = text.slice(0, at);
18
+ const kind = STORY_KINDS.find((candidate) => candidate === named);
19
+ return kind === void 0 ? null : storyKey(kind, text.slice(at + 1));
20
+ }
21
+ function isStoryJson(value) {
22
+ return typeof value === "object" && value !== null && typeof Reflect.get(value, "type") === "string";
23
+ }
24
+ function storiesOf(doc) {
25
+ const held = doc.attrs[STORIES_ATTR];
26
+ if (typeof held !== "object" || held === null) return {};
27
+ const stories = {};
28
+ for (const [key, value] of Object.entries(held)) {
29
+ if (asStoryKey(key) !== null && isStoryJson(value)) stories[key] = value;
30
+ }
31
+ return stories;
32
+ }
33
+ var built = /* @__PURE__ */ new WeakMap();
34
+ function storyNodeOf(doc, key) {
35
+ const json = storiesOf(doc)[key];
36
+ if (json === void 0) return null;
37
+ const cached = built.get(json);
38
+ if (cached) return cached;
39
+ const node = doc.type.schema.nodeFromJSON(json);
40
+ built.set(json, node);
41
+ return node;
42
+ }
43
+ var unkeyed = /* @__PURE__ */ new WeakMap();
44
+ function withoutBlockKeys(node) {
45
+ if (node.isText) return node;
46
+ const cached = unkeyed.get(node);
47
+ if (cached) return cached;
48
+ const children = node.children.map(withoutBlockKeys);
49
+ const attrs = node.attrs.srcId === null || node.attrs.srcId === void 0 ? node.attrs : { ...node.attrs, srcId: null };
50
+ const stripped = attrs === node.attrs && children.every((child, at) => child === node.child(at)) ? node : node.type.create(attrs, children, node.marks);
51
+ unkeyed.set(node, stripped);
52
+ return stripped;
53
+ }
54
+ function sameStory(a, b) {
55
+ if (a === null || b === null) return a === b;
56
+ return sameSource(withoutBlockKeys(a), withoutBlockKeys(b));
57
+ }
58
+ function withoutCommentStories(doc) {
59
+ const stories = storiesOf(doc);
60
+ const kept = Object.fromEntries(
61
+ Object.entries(stories).filter(([key]) => !key.startsWith("comment:"))
62
+ );
63
+ if (Object.keys(kept).length === Object.keys(stories).length) return doc;
64
+ return doc.type.create(
65
+ { ...doc.attrs, [STORIES_ATTR]: kept },
66
+ doc.content,
67
+ doc.marks
68
+ );
69
+ }
70
+ export {
71
+ STORIES_ATTR,
72
+ asStoryKey,
73
+ sameStory,
74
+ storiesOf,
75
+ storyKey,
76
+ storyNodeOf,
77
+ withoutCommentStories
78
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The inline wrappers a stretch of content stands inside, as the schema records them.
3
+ *
4
+ * A wrapper is an element that holds inline content and goes back out around the same content:
5
+ * a content control (`w:sdt`), a hyperlink (`w:hyperlink`). Each is a mark of its own, so each
6
+ * keeps the `inclusive` rule and the attrs its kind needs, and all of them share the `wrapper`
7
+ * group, so a node's `marks:` whitelist names the group rather than every kind in it and a new
8
+ * kind arrives as one mark spec.
9
+ *
10
+ * The nesting is recorded on the marks themselves: `depth` counts from the outermost wrapper the
11
+ * file wrote inwards. The order of a mark set cannot carry it - `Mark.addToSet` keeps insertion
12
+ * order only among marks of different types, `parseDOM` rebuilds a set from the page, and two
13
+ * wrappers of one kind may stand one inside the other - so `depth` is the source of truth and
14
+ * `wrapperMarks` is the one place that reads the order off it.
15
+ *
16
+ * What each kind reads out of the file and writes back into it is `docx/wrappers`.
17
+ */
18
+ import type { Mark, MarkType, Node as PMNode } from "prosemirror-model";
19
+ /** The mark group every inline wrapper belongs to */
20
+ export declare const WRAPPER_GROUP = "wrapper";
21
+ /** What every wrapper mark spec carries besides the attrs of its own kind */
22
+ export declare const WRAPPER_ATTRS: {
23
+ /** Outer-to-inner order as read from the file; equal depths fall back to declaration rank */
24
+ readonly depth: {
25
+ readonly default: 0;
26
+ };
27
+ /** Which occurrence this is in the opened document, so two wrappers written alike stay apart */
28
+ readonly key: {
29
+ readonly default: 0;
30
+ };
31
+ };
32
+ /** Whether marks of this type wrap the content they cover (`WRAPPER_GROUP`) */
33
+ export declare function isWrapperType(type: MarkType): boolean;
34
+ /**
35
+ * The wrappers this node stands inside, outermost first.
36
+ *
37
+ * Two wrappers written at the same depth - a control laid over a link during editing, say - fall
38
+ * back to the order the schema declares them in, which is the nesting the marks meant before
39
+ * `depth` existed.
40
+ */
41
+ export declare function wrapperMarks(node: PMNode): readonly Mark[];
42
+ /** Every wrapper of this kind the node stands inside, outermost first */
43
+ export declare function wrappersOf(node: PMNode, kind: MarkType): readonly Mark[];
44
+ /** The outermost wrapper of this kind the node stands inside. null where it stands in none */
45
+ export declare function wrapperOf(node: PMNode, kind: MarkType): Mark | null;
46
+ /**
47
+ * The depth of the innermost of these wrappers, and -1 where there is none, so that
48
+ * `innermostDepth(marks) + 1` is the depth a wrapper laid inside every one of them takes.
49
+ */
50
+ export declare function innermostDepth(marks: readonly Mark[]): number;
51
+ /**
52
+ * The wrappers all of these nodes stand inside alike, outermost first: the longest run of
53
+ * wrappers, from the outside in, that every one of them wears the very same one of.
54
+ *
55
+ * A wrapper laid across the whole stretch goes inside these and outside everything else, which is
56
+ * what keeps it one element in the file rather than one per stretch the nesting differs over.
57
+ */
58
+ export declare function sharedWrappers(nodes: readonly PMNode[]): readonly Mark[];
@@ -0,0 +1,77 @@
1
+ // src/schema/wrappers.ts
2
+ var WRAPPER_GROUP = "wrapper";
3
+ var WRAPPER_ATTRS = {
4
+ /** Outer-to-inner order as read from the file; equal depths fall back to declaration rank */
5
+ depth: { default: 0 },
6
+ /** Which occurrence this is in the opened document, so two wrappers written alike stay apart */
7
+ key: { default: 0 }
8
+ };
9
+ var FACTS = /* @__PURE__ */ new WeakMap();
10
+ var NO_WRAPPERS = [];
11
+ function factsOf(type) {
12
+ const known = FACTS.get(type);
13
+ if (known) return known;
14
+ const facts = {
15
+ wrapper: (type.spec.group ?? "").split(" ").includes(WRAPPER_GROUP),
16
+ rank: Object.keys(type.schema.marks).indexOf(type.name)
17
+ };
18
+ FACTS.set(type, facts);
19
+ return facts;
20
+ }
21
+ function isWrapperType(type) {
22
+ return factsOf(type).wrapper;
23
+ }
24
+ function depthOf(mark) {
25
+ const depth = mark.attrs.depth;
26
+ return typeof depth === "number" ? depth : 0;
27
+ }
28
+ function wrapperMarks(node) {
29
+ const marks = node.marks;
30
+ let found = 0;
31
+ for (const mark of marks) {
32
+ if (factsOf(mark.type).wrapper) found += 1;
33
+ }
34
+ if (found === 0) return NO_WRAPPERS;
35
+ const wrappers = found === marks.length ? [...marks] : marks.filter((mark) => factsOf(mark.type).wrapper);
36
+ if (found < 2) return wrappers;
37
+ return wrappers.sort(
38
+ (a, b) => depthOf(a) - depthOf(b) || factsOf(a.type).rank - factsOf(b.type).rank
39
+ );
40
+ }
41
+ function wrappersOf(node, kind) {
42
+ return wrapperMarks(node).filter((mark) => mark.type === kind);
43
+ }
44
+ function wrapperOf(node, kind) {
45
+ return wrapperMarks(node).find((mark) => mark.type === kind) ?? null;
46
+ }
47
+ function innermostDepth(marks) {
48
+ return marks.reduce(
49
+ (deepest, mark) => factsOf(mark.type).wrapper ? Math.max(deepest, depthOf(mark)) : deepest,
50
+ -1
51
+ );
52
+ }
53
+ function sharedWrappers(nodes) {
54
+ const first = nodes.at(0);
55
+ if (first === void 0) return [];
56
+ let shared = wrapperMarks(first);
57
+ for (const node of nodes.slice(1)) {
58
+ const marks = wrapperMarks(node);
59
+ const differs = shared.findIndex((mark, index) => {
60
+ const beside = marks[index];
61
+ return beside === void 0 || !mark.eq(beside);
62
+ });
63
+ if (differs !== -1) shared = shared.slice(0, differs);
64
+ if (shared.length === 0) break;
65
+ }
66
+ return shared;
67
+ }
68
+ export {
69
+ WRAPPER_ATTRS,
70
+ WRAPPER_GROUP,
71
+ innermostDepth,
72
+ isWrapperType,
73
+ sharedWrappers,
74
+ wrapperMarks,
75
+ wrapperOf,
76
+ wrappersOf
77
+ };
@@ -46,13 +46,11 @@ export declare const editorClassNames: {
46
46
  readonly commentMarker: "docx-editor-comment-marker";
47
47
  /** A superscript footnote or endnote reference */
48
48
  readonly noteReference: "docx-editor-note-reference";
49
- /** An invisible bookmark marker that occurs between body blocks */
50
- readonly bookmarkBlock: "docx-editor-bookmark-block";
51
49
  readonly rawInline: "docx-editor-raw-inline";
50
+ /** A preserved child of a run, drawn by the `display` its policy gave it */
51
+ readonly rawRunContent: "docx-editor-raw-run";
52
+ /** A preserved block, drawn by the `display` its policy gave it */
52
53
  readonly rawBlock: "docx-editor-raw-block";
53
- /** A preserved block that carries the original XML directly (inside a table cell, and so on) */
54
- readonly rawXmlBlock: "docx-editor-raw-xml";
55
- readonly tablePlaceholder: "docx-editor-table";
56
54
  /** The box holding both the paper and the marks overlaid on it */
57
55
  readonly pageLayer: "docx-editor-page-layer";
58
56
  /** All of the page marks drawn over the paper */
@@ -41,13 +41,11 @@ var editorClassNames = {
41
41
  commentMarker: `${PREFIX}-comment-marker`,
42
42
  /** A superscript footnote or endnote reference */
43
43
  noteReference: `${PREFIX}-note-reference`,
44
- /** An invisible bookmark marker that occurs between body blocks */
45
- bookmarkBlock: `${PREFIX}-bookmark-block`,
46
44
  rawInline: `${PREFIX}-raw-inline`,
45
+ /** A preserved child of a run, drawn by the `display` its policy gave it */
46
+ rawRunContent: `${PREFIX}-raw-run`,
47
+ /** A preserved block, drawn by the `display` its policy gave it */
47
48
  rawBlock: `${PREFIX}-raw-block`,
48
- /** A preserved block that carries the original XML directly (inside a table cell, and so on) */
49
- rawXmlBlock: `${PREFIX}-raw-xml`,
50
- tablePlaceholder: `${PREFIX}-table`,
51
49
  /** The box holding both the paper and the marks overlaid on it */
52
50
  pageLayer: `${PREFIX}-page-layer`,
53
51
  /** All of the page marks drawn over the paper */
package/dist/styles.css CHANGED
@@ -62,11 +62,6 @@
62
62
  overflow: hidden;
63
63
  }
64
64
 
65
- /* A body-level bookmark occupies a document-model position but draws no placeholder. */
66
- .docx-editor-bookmark-block {
67
- display: none;
68
- }
69
-
70
65
  .docx-editor-comment-range {
71
66
  background: #fff2a8;
72
67
  box-shadow: inset 0 -2px #e2b714;
@@ -588,10 +583,12 @@ button.docx-editor-comment-body {
588
583
  * is the same paper `A4_PORTRAIT` names in `docx/pageGeometry.ts`.
589
584
  *
590
585
  * The height is used only as a minimum, so that even short text feels like a full
591
- * sheet and longer text simply grows it.
592
- * The body left inside these margins is the width of a new table and the height
593
- * between page boundaries; `page/pageLayout.ts` works both out from the very same
594
- * geometry, so the divider on screen lands where the sheet actually ends.
586
+ * sheet and longer text simply grows it, and the editor supplies one grown to the
587
+ * last page's own bottom margin where a later section's is deeper than this one's.
588
+ * The body left inside these margins is what the sheet itself is drawn at. The width
589
+ * of a new table and the height between two page boundaries come from the paper of
590
+ * the section they stand in, which `page/pageLayout.ts` works out from the very same
591
+ * kind of geometry, so the divider on screen lands where that page actually ends.
595
592
  */
596
593
  --docx-editor-page-width: 21cm;
597
594
  --docx-editor-page-height: 29.7cm;
@@ -677,7 +674,10 @@ button.docx-editor-comment-body {
677
674
  border-top: 1px dotted rgb(26 79 208 / 40%);
678
675
  }
679
676
 
680
- /* Display-only projections of the first section's header and footer stories. */
677
+ /*
678
+ * Display-only projections of the header and footer stories of the section each page
679
+ * belongs to (`page/PageGuides.tsx`).
680
+ */
681
681
  .docx-editor-page-header,
682
682
  .docx-editor-page-footer {
683
683
  position: absolute;
@@ -928,12 +928,21 @@ button.docx-editor-comment-body {
928
928
  * held as data rather than as an `href`, so a click here places the caret the way a click on any
929
929
  * other text does, and opening the address is the link panel's business.
930
930
  * A color the run itself carries paints over this one, the same as any other run formatting.
931
+ * The line is a border rather than an underline so that it runs on across a tab, which a text
932
+ * decoration is not painted over.
931
933
  */
932
934
  .docx-editor-link {
933
935
  color: #1a56db;
934
936
  border-bottom: 1px solid currentcolor;
935
937
  }
936
938
 
939
+ /* A word processor underlines a link through the `Hyperlink` style, so the line above would be a second one */
940
+ .docx-editor-link:has([data-underline]):not(
941
+ :has(.docx-editor-run:not([data-underline]))
942
+ ) {
943
+ border-bottom: none;
944
+ }
945
+
937
946
  /*
938
947
  * A content control that says its contents may not be edited (`editor/plugins/lockedContent` refuses
939
948
  * the edits). Yellow is what the contract templates already paint their settled parts with.
@@ -957,15 +966,63 @@ button.docx-editor-comment-body {
957
966
  cursor: not-allowed;
958
967
  }
959
968
 
969
+ /*
970
+ * A preserved fragment inside a paragraph or inside a run. What it draws is the `display` the
971
+ * preservation rule gave it (`docx/importPolicy`), so the rule is read here as an attribute
972
+ * rather than as a second list of element names.
973
+ */
974
+ .docx-editor-raw-inline,
975
+ .docx-editor-raw-run {
976
+ vertical-align: baseline;
977
+ }
978
+
960
979
  /* Elements that, like bookmarks, must not be visible */
961
- .docx-editor-raw-inline {
980
+ .docx-editor-raw-inline[data-display="hidden"],
981
+ .docx-editor-raw-run[data-display="hidden"] {
962
982
  display: inline-block;
963
983
  width: 0;
964
984
  overflow: hidden;
985
+ }
986
+
987
+ /* A character the file stands for and the editor cannot type, drawn as itself */
988
+ .docx-editor-raw-inline[data-display="text"],
989
+ .docx-editor-raw-run[data-display="text"] {
990
+ white-space: pre;
991
+ }
992
+
993
+ /*
994
+ * Content carried through whole that the editor has no model for: a field, a tracked change, a
995
+ * symbol. Small enough to read the line around it, outlined so it is plainly not ordinary text.
996
+ *
997
+ * Deliberately not `user-select: none`. A chip that carries its content whole can be selected and
998
+ * deleted whole, and taking it out of the selection would leave a reader dragging across it with
999
+ * no way to reach it.
1000
+ */
1001
+ .docx-editor-raw-inline[data-display="chip"],
1002
+ .docx-editor-raw-run[data-display="chip"] {
1003
+ display: inline-block;
1004
+ max-width: 16em;
1005
+ overflow: hidden;
1006
+ padding: 0 3px;
1007
+ border: 1px dashed #9a9a9a;
1008
+ border-radius: 3px;
1009
+ background: #f2f2f2;
1010
+ color: #595959;
1011
+ font-size: 0.85em;
1012
+ font-family: system-ui, sans-serif;
1013
+ line-height: 1.4;
1014
+ white-space: nowrap;
1015
+ text-overflow: ellipsis;
965
1016
  vertical-align: baseline;
966
1017
  }
967
1018
 
968
- .docx-editor-table,
1019
+ /* The chip while it is the selection, which is how a reader sees what a delete would take */
1020
+ .docx-editor-raw-inline[data-display="chip"].ProseMirror-selectednode,
1021
+ .docx-editor-raw-run[data-display="chip"].ProseMirror-selectednode {
1022
+ outline: 2px solid #1a73e8;
1023
+ outline-offset: 1px;
1024
+ }
1025
+
969
1026
  .docx-editor-raw-block {
970
1027
  margin: 8pt 0;
971
1028
  padding: 10pt 12pt;
@@ -978,6 +1035,11 @@ button.docx-editor-comment-body {
978
1035
  user-select: none;
979
1036
  }
980
1037
 
1038
+ /* A range marker holds a spot between blocks and stands for nothing on the page. */
1039
+ .docx-editor-raw-block[data-display="hidden"] {
1040
+ display: none;
1041
+ }
1042
+
981
1043
  /*
982
1044
  * The toolbar.
983
1045
  *
@@ -29,7 +29,8 @@ export interface ColumnEdge {
29
29
  /**
30
30
  * The limit a table can be widened to (dxa).
31
31
  *
32
- * The body width of the paper the document names, minus the table indent (`w:tblInd`).
32
+ * The body width of the paper handed in, which is the section's the table stands in, minus
33
+ * the table indent (`w:tblInd`).
33
34
  * For a table already stored wider than that, its current width is the limit, so
34
35
  * dragging can only narrow it.
35
36
  */
@@ -49,7 +50,8 @@ export declare function resizedGridCols(gridCols: number[], col: number, width:
49
50
  * Because it is one single transaction, a single undo returns everything to the state
50
51
  * before the drag.
51
52
  *
52
- * The geometry is the paper the open document names, which is what the table is kept
53
- * inside. The editor layer reads it off the state; this layer is handed it.
53
+ * The geometry is the paper of the section the table stands in, which is what the table is
54
+ * kept inside. The editor layer reads it off the state (`editor/documentStyles`); this layer
55
+ * is handed it.
54
56
  */
55
57
  export declare function buildResizeColumnTransaction(state: EditorState, edge: ColumnEdge, geometry: PageGeometry): Transaction | null;
@@ -5,11 +5,9 @@ import { type CommentAuthor } from "../editor/commands/commentCommands";
5
5
  export interface CommentsPanelProps {
6
6
  view: EditorView;
7
7
  state: EditorState;
8
- composerOpen: boolean;
9
8
  /** Whose comments the composers write. Null for a reader, who is offered none */
10
9
  author: CommentAuthor | null;
11
- closeComposer: () => void;
12
10
  scrollContainer: HTMLElement | null;
13
11
  allCommentsOpen: boolean;
14
12
  }
15
- export declare function CommentsPanel({ view, state, composerOpen, author, closeComposer, scrollContainer, allCommentsOpen, }: CommentsPanelProps): ReactElement;
13
+ export declare function CommentsPanel({ view, state, author, scrollContainer, allCommentsOpen, }: CommentsPanelProps): ReactElement;
@@ -4,7 +4,6 @@ import { useMemo, useRef, useState } from "react";
4
4
  import {
5
5
  addComment,
6
6
  addCommentReply,
7
- documentComments,
8
7
  removeComment,
9
8
  removeCommentReply,
10
9
  selectComment,
@@ -12,6 +11,11 @@ import {
12
11
  updateComment,
13
12
  updateCommentReply
14
13
  } from "../editor/commands/commentCommands.js";
14
+ import {
15
+ closeCommentComposer,
16
+ commentComposerRange
17
+ } from "../editor/plugins/commentComposer.js";
18
+ import { commentProjection } from "../editor/plugins/commentDecorations.js";
15
19
  import { commentOwned } from "../schema/protection.js";
16
20
  import { protectionOf } from "../schema/protectionState.js";
17
21
  import { editorClassNames } from "../styles/classNames.js";
@@ -37,19 +41,19 @@ function run(view, command) {
37
41
  if (applied) view.focus();
38
42
  return applied;
39
43
  }
44
+ var REFUSED_COMMENT = "This comment could not be added. The text it was written for may have been changed or locked since.";
45
+ var REFUSED_REPLY = "This reply could not be added. The comment it answers may have been changed or deleted since.";
40
46
  function sameTarget(left, right) {
41
47
  return left?.commentId === right.commentId && left.replyId === right.replyId;
42
48
  }
43
49
  function CommentsPanel({
44
50
  view,
45
51
  state,
46
- composerOpen,
47
52
  author,
48
- closeComposer,
49
53
  scrollContainer,
50
54
  allCommentsOpen
51
55
  }) {
52
- const comments = useMemo(() => documentComments(state), [state.doc]);
56
+ const comments = commentProjection.read(state).comments;
53
57
  const rules = protectionOf(state);
54
58
  const writer = author != null && rules.protection !== "readOnly" ? author : null;
55
59
  const owned = (authorId) => writer !== null && commentOwned(rules, authorId);
@@ -57,7 +61,9 @@ function CommentsPanel({
57
61
  const [editing, setEditing] = useState(null);
58
62
  const [draft, setDraft] = useState("");
59
63
  const [replying, setReplying] = useState(null);
60
- const composerSelectionPos = composerOpen ? state.selection.from : -1;
64
+ const composerRange = commentComposerRange(state);
65
+ const composerOpen = composerRange !== null;
66
+ const composerSelectionPos = composerRange?.from ?? -1;
61
67
  const visible = useMemo(
62
68
  () => allCommentsOpen ? [...comments].sort((left, right) => {
63
69
  const leftTime = Date.parse(left.date ?? "");
@@ -112,7 +118,7 @@ function CommentsPanel({
112
118
  className: editorClassNames.commentsCanvas,
113
119
  style: { height: allCommentsOpen ? void 0 : `${canvasHeight}px` },
114
120
  children: [
115
- composerOpen && writer !== null && /* @__PURE__ */ jsx(
121
+ composerRange !== null && writer !== null && /* @__PURE__ */ jsx(
116
122
  "div",
117
123
  {
118
124
  className: editorClassNames.commentPosition,
@@ -126,16 +132,19 @@ function CommentsPanel({
126
132
  author: writer,
127
133
  label: "Comment text",
128
134
  submitLabel: "Comment",
129
- onClose: closeComposer,
135
+ onClose: () => run(view, closeCommentComposer),
130
136
  onSubmit: (text) => run(
131
137
  view,
132
- addComment({
133
- text,
134
- author: writer.name,
135
- authorId: writer.id,
136
- initials: writer.initials
137
- })
138
- )
138
+ addComment(
139
+ {
140
+ text,
141
+ author: writer.name,
142
+ authorId: writer.id,
143
+ initials: writer.initials
144
+ },
145
+ composerRange
146
+ )
147
+ ) ? null : REFUSED_COMMENT
139
148
  }
140
149
  )
141
150
  }
@@ -335,7 +344,7 @@ function CommentsPanel({
335
344
  authorId: writer.id,
336
345
  initials: writer.initials
337
346
  })
338
- )
347
+ ) ? null : REFUSED_REPLY
339
348
  }
340
349
  )
341
350
  ] })
@@ -1,12 +1,12 @@
1
1
  // src/ui/NotesPanel.tsx
2
- import { documentNotes } from "../editor/commands/noteQueries.js";
2
+ import { noteProjection } from "../editor/commands/noteQueries.js";
3
3
  import { editorClassNames } from "../styles/classNames.js";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  function NotesPanel({
6
6
  state,
7
7
  pageWidth
8
8
  }) {
9
- const notes = documentNotes(state);
9
+ const { notes } = noteProjection.read(state);
10
10
  if (notes.length === 0) return null;
11
11
  return /* @__PURE__ */ jsxs(
12
12
  "section",
@@ -9,7 +9,5 @@ export interface TextMenuProps {
9
9
  anchor: TextMenuAnchor;
10
10
  /** Whether the entries that lock and unlock a stretch of text are offered */
11
11
  allowLocking?: boolean;
12
- /** Opens the built-in composer for the selected text. */
13
- onAddComment?: () => void;
14
12
  }
15
- export declare function TextMenu({ view, state, anchor, allowLocking, onAddComment, }: TextMenuProps): ReactElement;
13
+ export declare function TextMenu({ view, state, anchor, allowLocking, }: TextMenuProps): ReactElement;
@@ -17,7 +17,7 @@ import {
17
17
  selectionTouchesLocked,
18
18
  unlockSelection
19
19
  } from "../editor/commands/lockCommands.js";
20
- import { insertClipboardData } from "../editor/externalClipboard.js";
20
+ import { openCommentComposer } from "../editor/plugins/commentComposer.js";
21
21
  import {
22
22
  closeTextMenu
23
23
  } from "../editor/plugins/textContextMenu.js";
@@ -62,8 +62,13 @@ async function clipboardContent() {
62
62
  async function pasteFromClipboard(view) {
63
63
  const content = await clipboardContent();
64
64
  if (!content.html && !content.text) return;
65
+ const data = new DataTransfer();
66
+ if (content.html) data.setData("text/html", content.html);
67
+ if (content.text) data.setData("text/plain", content.text);
68
+ const event = new ClipboardEvent("paste", { clipboardData: data });
65
69
  view.focus();
66
- insertClipboardData(view, content);
70
+ if (content.html && view.pasteHTML(content.html, event)) return;
71
+ if (content.text) view.pasteText(content.text, event);
67
72
  }
68
73
  function lockItem(lock, run) {
69
74
  const locking = {
@@ -90,8 +95,7 @@ function TextMenu({
90
95
  view,
91
96
  state,
92
97
  anchor,
93
- allowLocking = false,
94
- onAddComment
98
+ allowLocking = false
95
99
  }) {
96
100
  const box = useRef(null);
97
101
  const placement = usePanelAtPoint(box, anchor);
@@ -143,8 +147,8 @@ function TextMenu({
143
147
  {
144
148
  label: "Add comment",
145
149
  icon: MessageSquarePlus,
146
- enabled: !!onAddComment && canAddComment(state),
147
- run: () => onAddComment?.()
150
+ enabled: canAddComment(state),
151
+ run: () => run(openCommentComposer)
148
152
  }
149
153
  ]);
150
154
  if (allowLocking && bodyOpen) {
@@ -10,7 +10,12 @@ export interface CommentComposerProps {
10
10
  author: CommentAuthor;
11
11
  label: string;
12
12
  submitLabel: string;
13
- onSubmit: (text: string) => boolean;
13
+ /**
14
+ * Writes the comment, and answers why it was not written where it was not. The text stays in
15
+ * the form then, together with the reason, rather than the form standing there as if nothing
16
+ * had been asked of it.
17
+ */
18
+ onSubmit: (text: string) => string | null;
14
19
  onClose: () => void;
15
20
  }
16
21
  export declare function CommentComposer({ author, label, submitLabel, onSubmit, onClose, }: CommentComposerProps): ReactElement;
@@ -1,5 +1,6 @@
1
1
  // src/ui/comments/CommentComposer.tsx
2
2
  import {
3
+ useEffect,
3
4
  useLayoutEffect,
4
5
  useRef,
5
6
  useState
@@ -12,7 +13,15 @@ function FocusedTextarea({
12
13
  onChange
13
14
  }) {
14
15
  const input = useRef(null);
15
- useLayoutEffect(() => input.current?.focus(), []);
16
+ useLayoutEffect(() => input.current?.focus({ preventScroll: true }), []);
17
+ useEffect(() => {
18
+ const field = input.current;
19
+ if (typeof field?.scrollIntoView !== "function") return;
20
+ const frame = requestAnimationFrame(() => {
21
+ field.scrollIntoView({ block: "nearest" });
22
+ });
23
+ return () => cancelAnimationFrame(frame);
24
+ }, []);
16
25
  return /* @__PURE__ */ jsx(
17
26
  "textarea",
18
27
  {
@@ -32,13 +41,27 @@ function CommentComposer({
32
41
  onClose
33
42
  }) {
34
43
  const [text, setText] = useState("");
44
+ const [refused, setRefused] = useState(null);
35
45
  const submit = (event) => {
36
46
  event.preventDefault();
37
- if (onSubmit(text)) onClose();
47
+ const reason = onSubmit(text);
48
+ setRefused(reason);
49
+ if (reason === null) onClose();
38
50
  };
39
51
  return /* @__PURE__ */ jsxs("form", { className: editorClassNames.commentComposer, onSubmit: submit, children: [
40
52
  /* @__PURE__ */ jsx("div", { className: editorClassNames.commentMeta, children: author.name }),
41
- /* @__PURE__ */ jsx(FocusedTextarea, { label, value: text, onChange: setText }),
53
+ /* @__PURE__ */ jsx(
54
+ FocusedTextarea,
55
+ {
56
+ label,
57
+ value: text,
58
+ onChange: (next) => {
59
+ setRefused(null);
60
+ setText(next);
61
+ }
62
+ }
63
+ ),
64
+ refused !== null && /* @__PURE__ */ jsx("p", { className: editorClassNames.commentMeta, role: "alert", children: refused }),
42
65
  /* @__PURE__ */ jsxs("div", { className: editorClassNames.commentActions, children: [
43
66
  /* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, children: "Cancel" }),
44
67
  /* @__PURE__ */ jsx(