docxodus 8.0.0 → 9.0.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/README.md +65 -24
- package/dist/editor-headerfooter.d.ts +39 -3
- package/dist/editor-headerfooter.d.ts.map +1 -1
- package/dist/editor-headerfooter.js +118 -2
- package/dist/editor-headerfooter.js.map +1 -1
- package/dist/editor-reconcile.d.ts +75 -0
- package/dist/editor-reconcile.d.ts.map +1 -0
- package/dist/editor-reconcile.js +125 -0
- package/dist/editor-reconcile.js.map +1 -0
- package/dist/editor.bundle.js +799 -42
- package/dist/editor.d.ts +121 -5
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +572 -31
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/page-number-format.d.ts +22 -0
- package/dist/page-number-format.d.ts.map +1 -0
- package/dist/page-number-format.js +60 -0
- package/dist/page-number-format.js.map +1 -0
- package/dist/pagination.bundle.js +158 -21
- package/dist/pagination.d.ts +38 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +181 -29
- package/dist/pagination.js.map +1 -1
- package/dist/session.bundle.js +226 -4
- package/dist/session.d.ts +147 -6
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +187 -4
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +191 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +10 -10
- package/dist/wasm/_framework/dotnet.native.js.symbols +2102 -2101
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/package.json +17 -4
package/dist/editor.bundle.js
CHANGED
|
@@ -25,6 +25,40 @@ var DocxodusEditor = (() => {
|
|
|
25
25
|
serializeInlineMarkdown: () => serializeInlineMarkdown
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
// src/page-number-format.ts
|
|
29
|
+
var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
|
|
30
|
+
var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
|
|
31
|
+
var ROMAN_HUNDREDS = ["", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm"];
|
|
32
|
+
var ROMAN_THOUSANDS = ["", "m", "mm", "mmm"];
|
|
33
|
+
function toRoman(value) {
|
|
34
|
+
if (value <= 0 || value >= 4e3) return String(value);
|
|
35
|
+
return ROMAN_THOUSANDS[Math.floor(value / 1e3)] + ROMAN_HUNDREDS[Math.floor(value % 1e3 / 100)] + ROMAN_TENS[Math.floor(value % 100 / 10)] + ROMAN_ONES[value % 10];
|
|
36
|
+
}
|
|
37
|
+
function toLetter(value) {
|
|
38
|
+
if (value <= 0) return String(value);
|
|
39
|
+
const wrapped = value % 780 === 0 ? 780 : value % 780;
|
|
40
|
+
const repeats = Math.floor((wrapped - 1) / 26) + 1;
|
|
41
|
+
return "abcdefghijklmnopqrstuvwxyz".charAt((wrapped - 1) % 26).repeat(repeats);
|
|
42
|
+
}
|
|
43
|
+
var RENDERERS = {
|
|
44
|
+
// ST_NumberFormat tokens (w:pgNumType/@w:fmt, w:numFmt).
|
|
45
|
+
decimal: (v) => String(v),
|
|
46
|
+
lowerRoman: toRoman,
|
|
47
|
+
upperRoman: (v) => toRoman(v).toUpperCase(),
|
|
48
|
+
lowerLetter: toLetter,
|
|
49
|
+
upperLetter: (v) => toLetter(v).toUpperCase(),
|
|
50
|
+
// Field `\*` general-formatting switch arguments.
|
|
51
|
+
Arabic: (v) => String(v),
|
|
52
|
+
roman: toRoman,
|
|
53
|
+
ROMAN: (v) => toRoman(v).toUpperCase(),
|
|
54
|
+
alphabetic: toLetter,
|
|
55
|
+
ALPHABETIC: (v) => toLetter(v).toUpperCase()
|
|
56
|
+
};
|
|
57
|
+
function formatPageNumber(value, format) {
|
|
58
|
+
const renderer = format ? RENDERERS[format] : void 0;
|
|
59
|
+
return (renderer ?? RENDERERS.decimal)(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
// src/pagination.ts
|
|
29
63
|
var DEFAULT_PAGE_WIDTH = 612;
|
|
30
64
|
var DEFAULT_PAGE_HEIGHT = 792;
|
|
@@ -72,6 +106,8 @@ var DocxodusEditor = (() => {
|
|
|
72
106
|
*/
|
|
73
107
|
constructor(staging, container, options = {}) {
|
|
74
108
|
this.pendingFootnoteContinuation = null;
|
|
109
|
+
/** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
|
|
110
|
+
this.pageNumbering = /* @__PURE__ */ new Map();
|
|
75
111
|
this.stagingElement = typeof staging === "string" ? document.getElementById(staging) : staging;
|
|
76
112
|
this.containerElement = typeof container === "string" ? document.getElementById(container) : container;
|
|
77
113
|
if (!this.stagingElement) {
|
|
@@ -101,6 +137,7 @@ var DocxodusEditor = (() => {
|
|
|
101
137
|
const sections = this.stagingElement.querySelectorAll(
|
|
102
138
|
"[data-section-index]"
|
|
103
139
|
);
|
|
140
|
+
this.pageNumbering = this.parsePageNumbering(sections);
|
|
104
141
|
const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
|
|
105
142
|
for (const section of sectionsToProcess) {
|
|
106
143
|
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
@@ -116,8 +153,62 @@ var DocxodusEditor = (() => {
|
|
|
116
153
|
pageNumber += sectionPages.length;
|
|
117
154
|
}
|
|
118
155
|
this.stagingElement.style.display = "none";
|
|
156
|
+
this.substitutePageNumberFields(pages.length);
|
|
119
157
|
return { totalPages: pages.length, pages };
|
|
120
158
|
}
|
|
159
|
+
/** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
|
|
160
|
+
parsePageNumbering(sections) {
|
|
161
|
+
const map = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const section of Array.from(sections)) {
|
|
163
|
+
const index = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
164
|
+
const rawStart = section.dataset.pageNumStart;
|
|
165
|
+
const start = rawStart === void 0 ? void 0 : parseInt(rawStart, 10);
|
|
166
|
+
map.set(index, {
|
|
167
|
+
start: start !== void 0 && Number.isFinite(start) ? start : void 0,
|
|
168
|
+
format: section.dataset.pageNumFmt
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return map;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Fill in the page-number fields inside every page's cloned header/footer.
|
|
175
|
+
*
|
|
176
|
+
* A header/footer is authored once and cloned onto each page, so a PAGE field's single cached
|
|
177
|
+
* result would otherwise show the same number on every page — the whole reason the converter
|
|
178
|
+
* marks these. `data-field-format` (the field's own `\*` switch) wins over the section's format
|
|
179
|
+
* when present, which is exactly how Word resolves the two.
|
|
180
|
+
*
|
|
181
|
+
* Runs after layout because NUMPAGES cannot be known before the last page exists. The
|
|
182
|
+
* substituted text can therefore be marginally wider than the cached result the header was
|
|
183
|
+
* measured with; the header band clips, so the failure mode is a hair of overflow rather than
|
|
184
|
+
* a layout that disagrees with itself.
|
|
185
|
+
*
|
|
186
|
+
* Scoped to the CLONED header/footer regions on purpose. A page-number field in body text is
|
|
187
|
+
* ordinary run content that the editor may make editable, and committing an edited block writes
|
|
188
|
+
* back whatever text the DOM holds — rewriting it here would mean a body field commits a number
|
|
189
|
+
* the document never contained. Body content is also not cloned, so it does not have the problem
|
|
190
|
+
* this method exists to solve.
|
|
191
|
+
*/
|
|
192
|
+
substitutePageNumberFields(totalPages) {
|
|
193
|
+
const boxes = this.containerElement.querySelectorAll(`.${this.cssPrefix}box`);
|
|
194
|
+
for (const box of Array.from(boxes)) {
|
|
195
|
+
const markers = box.querySelectorAll(
|
|
196
|
+
`.${this.cssPrefix}header [data-field], .${this.cssPrefix}footer [data-field]`
|
|
197
|
+
);
|
|
198
|
+
if (markers.length === 0) continue;
|
|
199
|
+
const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
|
|
200
|
+
const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
|
|
201
|
+
const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
|
|
202
|
+
const numbering = this.pageNumbering.get(sectionIndex) ?? {};
|
|
203
|
+
const displayed = numbering.start !== void 0 ? numbering.start + pageInSection - 1 : pageNumber;
|
|
204
|
+
for (const marker of Array.from(markers)) {
|
|
205
|
+
const kind = marker.dataset.field;
|
|
206
|
+
if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
|
|
207
|
+
const format = marker.dataset.fieldFormat ?? numbering.format;
|
|
208
|
+
marker.textContent = formatPageNumber(kind === "PAGE" ? displayed : totalPages, format);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
121
212
|
/**
|
|
122
213
|
* Measures all content blocks in a section.
|
|
123
214
|
*/
|
|
@@ -641,6 +732,7 @@ var DocxodusEditor = (() => {
|
|
|
641
732
|
measureContainer.style.visibility = "hidden";
|
|
642
733
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
643
734
|
measureContainer.style.left = "-9999px";
|
|
735
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
644
736
|
const hr = document.createElement("hr");
|
|
645
737
|
measureContainer.appendChild(hr);
|
|
646
738
|
if (hasContinuation) {
|
|
@@ -675,6 +767,7 @@ var DocxodusEditor = (() => {
|
|
|
675
767
|
measureContainer.style.visibility = "hidden";
|
|
676
768
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
677
769
|
measureContainer.style.left = "-9999px";
|
|
770
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
678
771
|
const hr = document.createElement("hr");
|
|
679
772
|
measureContainer.appendChild(hr);
|
|
680
773
|
for (const el of continuation.remainingElements) {
|
|
@@ -693,11 +786,17 @@ var DocxodusEditor = (() => {
|
|
|
693
786
|
splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
|
|
694
787
|
const footnoteContent = footnoteElement.querySelector(".footnote-content");
|
|
695
788
|
if (!footnoteContent) {
|
|
696
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
|
|
791
|
+
overflow: []
|
|
792
|
+
};
|
|
697
793
|
}
|
|
698
794
|
const children = Array.from(footnoteContent.children);
|
|
699
795
|
if (children.length <= 1) {
|
|
700
|
-
return {
|
|
796
|
+
return {
|
|
797
|
+
fits: children.map((el) => el.cloneNode(true)),
|
|
798
|
+
overflow: []
|
|
799
|
+
};
|
|
701
800
|
}
|
|
702
801
|
const fits = [];
|
|
703
802
|
const overflow = [];
|
|
@@ -707,6 +806,7 @@ var DocxodusEditor = (() => {
|
|
|
707
806
|
hrMeasure.style.visibility = "hidden";
|
|
708
807
|
hrMeasure.style.width = `${contentWidth}pt`;
|
|
709
808
|
hrMeasure.style.left = "-9999px";
|
|
809
|
+
hrMeasure.className = this.cssPrefix + "footnotes";
|
|
710
810
|
const hr = document.createElement("hr");
|
|
711
811
|
hrMeasure.appendChild(hr);
|
|
712
812
|
this.stagingElement.appendChild(hrMeasure);
|
|
@@ -721,6 +821,7 @@ var DocxodusEditor = (() => {
|
|
|
721
821
|
measureContainer.style.visibility = "hidden";
|
|
722
822
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
723
823
|
measureContainer.style.left = "-9999px";
|
|
824
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
724
825
|
measureContainer.appendChild(child.cloneNode(true));
|
|
725
826
|
this.stagingElement.appendChild(measureContainer);
|
|
726
827
|
const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
|
|
@@ -748,6 +849,7 @@ var DocxodusEditor = (() => {
|
|
|
748
849
|
measureContainer.style.visibility = "hidden";
|
|
749
850
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
750
851
|
measureContainer.style.left = "-9999px";
|
|
852
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
751
853
|
measureContainer.appendChild(footnote.cloneNode(true));
|
|
752
854
|
this.stagingElement.appendChild(measureContainer);
|
|
753
855
|
const rect = measureContainer.getBoundingClientRect();
|
|
@@ -931,6 +1033,7 @@ var DocxodusEditor = (() => {
|
|
|
931
1033
|
let currentFootnoteHeight = 0;
|
|
932
1034
|
let currentContinuation = this.pendingFootnoteContinuation;
|
|
933
1035
|
let nextPageContinuation = null;
|
|
1036
|
+
let deferredFootnoteIds = [];
|
|
934
1037
|
let currentPartialFootnotes = [];
|
|
935
1038
|
if (currentContinuation && currentContinuation.remainingElements.length > 0) {
|
|
936
1039
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
@@ -961,11 +1064,22 @@ var DocxodusEditor = (() => {
|
|
|
961
1064
|
currentPartialFootnotes = [];
|
|
962
1065
|
currentContinuation = nextPageContinuation;
|
|
963
1066
|
nextPageContinuation = null;
|
|
1067
|
+
if (deferredFootnoteIds.length > 0) {
|
|
1068
|
+
currentFootnoteIds = [...deferredFootnoteIds];
|
|
1069
|
+
deferredFootnoteIds = [];
|
|
1070
|
+
}
|
|
964
1071
|
if (currentContinuation && currentContinuation.remainingElements.length > 0) {
|
|
965
1072
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
966
1073
|
} else {
|
|
967
1074
|
currentFootnoteHeight = 0;
|
|
968
1075
|
}
|
|
1076
|
+
if (currentFootnoteIds.length > 0) {
|
|
1077
|
+
currentFootnoteHeight += this.measureFootnotesHeight(
|
|
1078
|
+
currentFootnoteIds,
|
|
1079
|
+
dims.contentWidth,
|
|
1080
|
+
null
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
969
1083
|
};
|
|
970
1084
|
for (let i = 0; i < blocks.length; i++) {
|
|
971
1085
|
const block = blocks[i];
|
|
@@ -1111,22 +1225,10 @@ var DocxodusEditor = (() => {
|
|
|
1111
1225
|
}
|
|
1112
1226
|
currentFootnoteHeight = availableForFootnotes;
|
|
1113
1227
|
} else {
|
|
1114
|
-
|
|
1115
|
-
footnoteId,
|
|
1116
|
-
remainingElements: Array.from(footnote.querySelectorAll(".footnote-content > *")).map((el) => el.cloneNode(true))
|
|
1117
|
-
};
|
|
1118
|
-
if (nextPageContinuation.remainingElements.length === 0) {
|
|
1119
|
-
nextPageContinuation.remainingElements = [footnote.cloneNode(true)];
|
|
1120
|
-
}
|
|
1228
|
+
deferredFootnoteIds.push(footnoteId);
|
|
1121
1229
|
}
|
|
1122
1230
|
} else {
|
|
1123
|
-
|
|
1124
|
-
footnoteId,
|
|
1125
|
-
remainingElements: Array.from(footnote.querySelectorAll(".footnote-content > *")).map((el) => el.cloneNode(true))
|
|
1126
|
-
};
|
|
1127
|
-
if (nextPageContinuation.remainingElements.length === 0) {
|
|
1128
|
-
nextPageContinuation.remainingElements = [footnote.cloneNode(true)];
|
|
1129
|
-
}
|
|
1231
|
+
deferredFootnoteIds.push(footnoteId);
|
|
1130
1232
|
}
|
|
1131
1233
|
}
|
|
1132
1234
|
}
|
|
@@ -1146,7 +1248,7 @@ var DocxodusEditor = (() => {
|
|
|
1146
1248
|
currentContent.push(block.element.cloneNode(true));
|
|
1147
1249
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1148
1250
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1149
|
-
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
1251
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1150
1252
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
1151
1253
|
}
|
|
1152
1254
|
} else {
|
|
@@ -1156,7 +1258,7 @@ var DocxodusEditor = (() => {
|
|
|
1156
1258
|
currentContent.push(block.element.cloneNode(true));
|
|
1157
1259
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1158
1260
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1159
|
-
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
1261
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1160
1262
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
1161
1263
|
}
|
|
1162
1264
|
} else {
|
|
@@ -1173,7 +1275,7 @@ var DocxodusEditor = (() => {
|
|
|
1173
1275
|
finishPage();
|
|
1174
1276
|
}
|
|
1175
1277
|
currentContent.push(block.element.cloneNode(true));
|
|
1176
|
-
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
1278
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1177
1279
|
finishPage();
|
|
1178
1280
|
}
|
|
1179
1281
|
}
|
|
@@ -1181,6 +1283,27 @@ var DocxodusEditor = (() => {
|
|
|
1181
1283
|
this.pendingFootnoteContinuation = nextPageContinuation;
|
|
1182
1284
|
return pages;
|
|
1183
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Strip block addressing from a header/footer node cloned into a page box.
|
|
1288
|
+
*
|
|
1289
|
+
* A running story is authored ONCE and cloned onto every page, so the clones all carry the same
|
|
1290
|
+
* `data-anchor` — on this document, 42 page boxes claiming one footer paragraph. Left editable,
|
|
1291
|
+
* committing any one of them writes back through that single shared anchor, and the per-page
|
|
1292
|
+
* page-number substitution makes it worse: each clone shows a DIFFERENT number, so a commit
|
|
1293
|
+
* writes that page's number into the story as literal text and destroys the PAGE field.
|
|
1294
|
+
*
|
|
1295
|
+
* Page-box header/footer content is presentation. The docked editing bands
|
|
1296
|
+
* (`editor-headerfooter.ts`) are the addressable affordance, and they exist precisely because a
|
|
1297
|
+
* cloned node cannot be uniquely addressed.
|
|
1298
|
+
*/
|
|
1299
|
+
makeClonedStoryInert(root) {
|
|
1300
|
+
const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
1301
|
+
for (const el of nodes) {
|
|
1302
|
+
el.removeAttribute("data-anchor");
|
|
1303
|
+
el.removeAttribute("data-committed-text");
|
|
1304
|
+
if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1184
1307
|
/**
|
|
1185
1308
|
* Creates a page container element.
|
|
1186
1309
|
*/
|
|
@@ -1206,6 +1329,7 @@ var DocxodusEditor = (() => {
|
|
|
1206
1329
|
pageBox.style.contain = "layout paint";
|
|
1207
1330
|
pageBox.dataset.pageNumber = String(pageNumber);
|
|
1208
1331
|
pageBox.dataset.sectionIndex = String(sectionIndex);
|
|
1332
|
+
pageBox.dataset.pageInSection = String(pageInSection);
|
|
1209
1333
|
const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
|
|
1210
1334
|
const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
|
|
1211
1335
|
if (headerSource) {
|
|
@@ -1223,7 +1347,9 @@ var DocxodusEditor = (() => {
|
|
|
1223
1347
|
headerDiv.style.justifyContent = "flex-end";
|
|
1224
1348
|
headerDiv.style.paddingBottom = "4pt";
|
|
1225
1349
|
for (const child of Array.from(headerSource.childNodes)) {
|
|
1226
|
-
|
|
1350
|
+
const clonedheaderDiv = child.cloneNode(true);
|
|
1351
|
+
if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
|
|
1352
|
+
headerDiv.appendChild(clonedheaderDiv);
|
|
1227
1353
|
}
|
|
1228
1354
|
pageBox.appendChild(headerDiv);
|
|
1229
1355
|
}
|
|
@@ -1261,7 +1387,9 @@ var DocxodusEditor = (() => {
|
|
|
1261
1387
|
footerDiv.style.justifyContent = "flex-start";
|
|
1262
1388
|
footerDiv.style.paddingTop = "4pt";
|
|
1263
1389
|
for (const child of Array.from(footerSource.childNodes)) {
|
|
1264
|
-
|
|
1390
|
+
const clonedfooterDiv = child.cloneNode(true);
|
|
1391
|
+
if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
|
|
1392
|
+
footerDiv.appendChild(clonedfooterDiv);
|
|
1265
1393
|
}
|
|
1266
1394
|
pageBox.appendChild(footerDiv);
|
|
1267
1395
|
}
|
|
@@ -1272,6 +1400,13 @@ var DocxodusEditor = (() => {
|
|
|
1272
1400
|
pageBox.appendChild(pageNum);
|
|
1273
1401
|
}
|
|
1274
1402
|
this.containerElement.appendChild(pageBox);
|
|
1403
|
+
const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
|
|
1404
|
+
if (notesEl) {
|
|
1405
|
+
const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
|
|
1406
|
+
if (notesHeightPt > 0) {
|
|
1407
|
+
contentArea.style.height = `${Math.max(0, contentAreaHeight - notesHeightPt)}pt`;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1275
1410
|
return {
|
|
1276
1411
|
pageNumber,
|
|
1277
1412
|
sectionIndex,
|
|
@@ -1305,6 +1440,14 @@ var DocxodusEditor = (() => {
|
|
|
1305
1440
|
}
|
|
1306
1441
|
|
|
1307
1442
|
// src/editor-headerfooter.ts
|
|
1443
|
+
var PAGE_FORMAT_LABELS = [
|
|
1444
|
+
{ value: "", label: "Format\u2026" },
|
|
1445
|
+
{ value: "decimal", label: "1, 2, 3" },
|
|
1446
|
+
{ value: "lowerLetter", label: "a, b, c" },
|
|
1447
|
+
{ value: "upperLetter", label: "A, B, C" },
|
|
1448
|
+
{ value: "lowerRoman", label: "i, ii, iii" },
|
|
1449
|
+
{ value: "upperRoman", label: "I, II, III" }
|
|
1450
|
+
];
|
|
1308
1451
|
var KIND_LABELS = [
|
|
1309
1452
|
{ value: "default", label: "Default" },
|
|
1310
1453
|
{ value: "first", label: "First page" },
|
|
@@ -1378,13 +1521,63 @@ var DocxodusEditor = (() => {
|
|
|
1378
1521
|
kindOf(which) {
|
|
1379
1522
|
return this.kinds[which];
|
|
1380
1523
|
}
|
|
1381
|
-
/** Append a PAGE / NUMPAGES field to the story paragraph addressed by `anchorId`.
|
|
1524
|
+
/** Append a PAGE / NUMPAGES field to the story paragraph addressed by `anchorId`.
|
|
1525
|
+
* Deliberately a PLAIN field (no `\*` switch), exactly as Word inserts one, so it follows the
|
|
1526
|
+
* section's page-number format — see {@link setPageNumbering}. Stamping the section's current
|
|
1527
|
+
* format as a switch here would silently WIN over any later format change. */
|
|
1382
1528
|
insertPageNumber(which, anchorId, field) {
|
|
1383
|
-
const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field));
|
|
1529
|
+
const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
|
|
1384
1530
|
if (!res.success) return false;
|
|
1385
1531
|
this.refresh(which);
|
|
1386
1532
|
return true;
|
|
1387
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Set this section's page numbering (`w:pgNumType`) — Word's *Format Page Numbers…*. Omitted
|
|
1536
|
+
* fields are left alone, so the format and the start are independently settable. Both bands then
|
|
1537
|
+
* repaint, because the values belong to the section rather than to either story.
|
|
1538
|
+
*
|
|
1539
|
+
* The rendered page numbers in the editor do not change: a page-number field's cached result is
|
|
1540
|
+
* what the browser shows, and Word recomputes it on open. Paginated mode substitutes the real
|
|
1541
|
+
* per-page number, so it does reflect the format immediately.
|
|
1542
|
+
*/
|
|
1543
|
+
setPageNumbering(op) {
|
|
1544
|
+
if (!this.bodyAnchorId) return false;
|
|
1545
|
+
const res = parseResult(
|
|
1546
|
+
this.bridge.SetPageNumbering(this.handle, this.bodyAnchorId, JSON.stringify(op))
|
|
1547
|
+
);
|
|
1548
|
+
if (!res.success) return false;
|
|
1549
|
+
this.reloadSection();
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
/** This section's page numbering as the live document states it. Fields are absent, not
|
|
1553
|
+
* defaulted — "continues the previous section" is not the same claim as "starts at 1". */
|
|
1554
|
+
pageNumbering() {
|
|
1555
|
+
return {
|
|
1556
|
+
start: this.sectionInfo?.pageNumberStart,
|
|
1557
|
+
format: this.sectionInfo?.pageNumberFormat
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
/** Remove this section's page-numbering start/format — it reverts to continuing the previous
|
|
1561
|
+
* section's numbering in Word's default `1, 2, 3`. */
|
|
1562
|
+
clearPageNumbering() {
|
|
1563
|
+
if (!this.bodyAnchorId) return false;
|
|
1564
|
+
const res = parseResult(this.bridge.ClearPageNumbering(this.handle, this.bodyAnchorId));
|
|
1565
|
+
if (!res.success) return false;
|
|
1566
|
+
this.reloadSection();
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Re-read the section from the live document and repaint BOTH bands.
|
|
1571
|
+
*
|
|
1572
|
+
* `refreshAll` only repaints — it deliberately does not re-read, because its callers (remount,
|
|
1573
|
+
* undo/redo) already refreshed the section. A section-property write has no such caller, and
|
|
1574
|
+
* repainting from the stale snapshot would leave the chrome reporting the value the document had
|
|
1575
|
+
* before the edit.
|
|
1576
|
+
*/
|
|
1577
|
+
reloadSection() {
|
|
1578
|
+
this.sectionInfo = this.bodyAnchorId ? this.readSectionInfo(this.bodyAnchorId) : null;
|
|
1579
|
+
this.refreshAll();
|
|
1580
|
+
}
|
|
1388
1581
|
/** Insert a page number into a band's own target paragraph (focused, else last). Seeds the
|
|
1389
1582
|
* story first if the band's selected kind has none, so the command is never a silent no-op. */
|
|
1390
1583
|
insertPageNumberInBand(which, field) {
|
|
@@ -1509,6 +1702,41 @@ var DocxodusEditor = (() => {
|
|
|
1509
1702
|
if (target) this.insertPageNumber(which, target, field);
|
|
1510
1703
|
});
|
|
1511
1704
|
chrome.appendChild(pageNum);
|
|
1705
|
+
const pageFmt = document.createElement("select");
|
|
1706
|
+
pageFmt.setAttribute("data-hf-pagefmt", "");
|
|
1707
|
+
pageFmt.title = "Page-number format for this section";
|
|
1708
|
+
for (const o of PAGE_FORMAT_LABELS) {
|
|
1709
|
+
const opt = document.createElement("option");
|
|
1710
|
+
opt.value = o.value;
|
|
1711
|
+
opt.textContent = o.label;
|
|
1712
|
+
pageFmt.appendChild(opt);
|
|
1713
|
+
}
|
|
1714
|
+
pageFmt.addEventListener("change", () => {
|
|
1715
|
+
if (pageFmt.value === "") return;
|
|
1716
|
+
this.setPageNumbering({ format: pageFmt.value });
|
|
1717
|
+
});
|
|
1718
|
+
chrome.appendChild(pageFmt);
|
|
1719
|
+
const pageStart = document.createElement("input");
|
|
1720
|
+
pageStart.setAttribute("data-hf-pagestart", "");
|
|
1721
|
+
pageStart.type = "number";
|
|
1722
|
+
pageStart.min = "0";
|
|
1723
|
+
pageStart.placeholder = "Start at";
|
|
1724
|
+
pageStart.title = "Restart this section's page numbering at this number";
|
|
1725
|
+
const commitStart = () => {
|
|
1726
|
+
const raw = pageStart.value.trim();
|
|
1727
|
+
if (raw === "") return;
|
|
1728
|
+
const start = Number(raw);
|
|
1729
|
+
if (!Number.isInteger(start) || start < 0) return;
|
|
1730
|
+
this.setPageNumbering({ start });
|
|
1731
|
+
};
|
|
1732
|
+
pageStart.addEventListener("blur", commitStart);
|
|
1733
|
+
pageStart.addEventListener("keydown", (e) => {
|
|
1734
|
+
if (e.key === "Enter") {
|
|
1735
|
+
e.preventDefault();
|
|
1736
|
+
commitStart();
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
chrome.appendChild(pageStart);
|
|
1512
1740
|
const inheritedNote = document.createElement("span");
|
|
1513
1741
|
inheritedNote.className = "docx-hf-inherited";
|
|
1514
1742
|
inheritedNote.setAttribute("data-hf-inherited-note", "");
|
|
@@ -1546,6 +1774,13 @@ var DocxodusEditor = (() => {
|
|
|
1546
1774
|
const kind = this.kinds[which];
|
|
1547
1775
|
const select = band.querySelector("[data-hf-kind]");
|
|
1548
1776
|
if (select && select.value !== kind) select.value = kind;
|
|
1777
|
+
const pageFmt = band.querySelector("[data-hf-pagefmt]");
|
|
1778
|
+
if (pageFmt) pageFmt.value = this.sectionInfo?.pageNumberFormat ?? "";
|
|
1779
|
+
const pageStart = band.querySelector("[data-hf-pagestart]");
|
|
1780
|
+
if (pageStart && document.activeElement !== pageStart) {
|
|
1781
|
+
const start = this.sectionInfo?.pageNumberStart;
|
|
1782
|
+
pageStart.value = start === void 0 ? "" : String(start);
|
|
1783
|
+
}
|
|
1549
1784
|
const inherited = this.refFor(which, kind)?.inherited === true;
|
|
1550
1785
|
band.toggleAttribute("data-hf-inherited", inherited);
|
|
1551
1786
|
const note = band.querySelector("[data-hf-inherited-note]");
|
|
@@ -1651,6 +1886,79 @@ var DocxodusEditor = (() => {
|
|
|
1651
1886
|
}
|
|
1652
1887
|
}
|
|
1653
1888
|
|
|
1889
|
+
// src/editor-reconcile.ts
|
|
1890
|
+
function tokenOf(unit) {
|
|
1891
|
+
return unidOf(unit.id) + (unit.sig ? "|" + unit.sig : "");
|
|
1892
|
+
}
|
|
1893
|
+
function unidOf(id) {
|
|
1894
|
+
return id.substring(id.lastIndexOf(":") + 1);
|
|
1895
|
+
}
|
|
1896
|
+
function diffUnits(oldUnids, newUnits) {
|
|
1897
|
+
const n = oldUnids.length;
|
|
1898
|
+
const m = newUnits.length;
|
|
1899
|
+
const newUnids = newUnits.map(tokenOf);
|
|
1900
|
+
const lcs = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
|
|
1901
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
1902
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
1903
|
+
lcs[i2][j2] = oldUnids[i2] === newUnids[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const keep = /* @__PURE__ */ new Map();
|
|
1907
|
+
const removed = [];
|
|
1908
|
+
const added = [];
|
|
1909
|
+
let i = 0;
|
|
1910
|
+
let j = 0;
|
|
1911
|
+
while (i < n && j < m) {
|
|
1912
|
+
if (oldUnids[i] === newUnids[j]) {
|
|
1913
|
+
keep.set(j, i);
|
|
1914
|
+
i++;
|
|
1915
|
+
j++;
|
|
1916
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
1917
|
+
removed.push(i++);
|
|
1918
|
+
} else {
|
|
1919
|
+
added.push(j++);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
while (i < n) removed.push(i++);
|
|
1923
|
+
while (j < m) added.push(j++);
|
|
1924
|
+
const keptBeforeOld = (oi) => {
|
|
1925
|
+
let c = 0;
|
|
1926
|
+
for (const v of keep.values()) if (v < oi) c++;
|
|
1927
|
+
return c;
|
|
1928
|
+
};
|
|
1929
|
+
const keptBeforeNew = (nj) => {
|
|
1930
|
+
let c = 0;
|
|
1931
|
+
for (const k of keep.keys()) if (k < nj) c++;
|
|
1932
|
+
return c;
|
|
1933
|
+
};
|
|
1934
|
+
const substituted = [];
|
|
1935
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
1936
|
+
for (const nj of added) {
|
|
1937
|
+
const target = keptBeforeNew(nj);
|
|
1938
|
+
for (const oi of removed) {
|
|
1939
|
+
if (usedRemoved.has(oi)) continue;
|
|
1940
|
+
if (keptBeforeOld(oi) === target) {
|
|
1941
|
+
substituted.push({ oldIndex: oi, newIndex: nj });
|
|
1942
|
+
usedRemoved.add(oi);
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return { keep, removed, added, substituted };
|
|
1948
|
+
}
|
|
1949
|
+
function needsRemount(diff, newUnits, oldKinds, threshold = 40) {
|
|
1950
|
+
if (diff.added.length + diff.removed.length > threshold) return true;
|
|
1951
|
+
const subNew = new Set(diff.substituted.map((s) => s.newIndex));
|
|
1952
|
+
const subOld = new Set(diff.substituted.map((s) => s.oldIndex));
|
|
1953
|
+
for (const j of diff.added) {
|
|
1954
|
+
if (!subNew.has(j) && newUnits[j]?.kind === "li") return true;
|
|
1955
|
+
}
|
|
1956
|
+
for (const i of diff.removed) {
|
|
1957
|
+
if (!subOld.has(i) && oldKinds[i] === "li") return true;
|
|
1958
|
+
}
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1654
1962
|
// src/editor.ts
|
|
1655
1963
|
var EDITABLE_TAGS = /* @__PURE__ */ new Set(["P", "H1", "H2", "H3", "H4", "H5", "H6"]);
|
|
1656
1964
|
function fontWeightIsBold(w) {
|
|
@@ -1663,7 +1971,7 @@ var DocxodusEditor = (() => {
|
|
|
1663
1971
|
}
|
|
1664
1972
|
function collectInlineSegments(node, out) {
|
|
1665
1973
|
node.childNodes.forEach((child) => {
|
|
1666
|
-
if (child
|
|
1974
|
+
if (isGeneratedChrome(child)) return;
|
|
1667
1975
|
if (child.nodeType === 3) {
|
|
1668
1976
|
const text = child.textContent ?? "";
|
|
1669
1977
|
if (!text) return;
|
|
@@ -1717,10 +2025,14 @@ var DocxodusEditor = (() => {
|
|
|
1717
2025
|
function isListBlock(block) {
|
|
1718
2026
|
return !!block.querySelector(":scope > [data-list-marker]");
|
|
1719
2027
|
}
|
|
2028
|
+
var GENERATED_CHROME_SELECTOR = '[data-list-marker], a.footnote-ref, a.endnote-ref, a[class$="-backref"]';
|
|
2029
|
+
function isGeneratedChrome(node) {
|
|
2030
|
+
return node?.nodeType === 1 && !!node.matches?.(GENERATED_CHROME_SELECTOR);
|
|
2031
|
+
}
|
|
1720
2032
|
function isInMarker(node) {
|
|
1721
2033
|
let el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
1722
2034
|
while (el) {
|
|
1723
|
-
if (el
|
|
2035
|
+
if (isGeneratedChrome(el)) return true;
|
|
1724
2036
|
el = el.parentElement;
|
|
1725
2037
|
}
|
|
1726
2038
|
return false;
|
|
@@ -1952,8 +2264,11 @@ var DocxodusEditor = (() => {
|
|
|
1952
2264
|
false,
|
|
1953
2265
|
0,
|
|
1954
2266
|
"annot-",
|
|
2267
|
+
// Footnotes/endnotes ON: they are document content, and the editor makes the rendered note
|
|
2268
|
+
// paragraphs editable. Must stay in step with DocxSessionOps.RenderHtml (the remount path),
|
|
2269
|
+
// whose output has to match this first paint byte-for-byte.
|
|
1955
2270
|
/* renderFootnotesAndEndnotes */
|
|
1956
|
-
|
|
2271
|
+
true,
|
|
1957
2272
|
/* renderHeadersAndFooters */
|
|
1958
2273
|
paginated,
|
|
1959
2274
|
false,
|
|
@@ -1989,6 +2304,9 @@ var DocxodusEditor = (() => {
|
|
|
1989
2304
|
this.lastSelection = null;
|
|
1990
2305
|
/** The docked header/footer bands, when `options.headerFooter` is on. */
|
|
1991
2306
|
this.region = null;
|
|
2307
|
+
/** Why the last reconcile() fell back to a full remount (null = it patched). For
|
|
2308
|
+
* diagnostics/specs; not part of the public API. */
|
|
2309
|
+
this.lastReconcileFallback = null;
|
|
1992
2310
|
/** Track the last meaningful selection so focus-stealing toolbar controls can still target it. */
|
|
1993
2311
|
this.onSelectionChange = () => {
|
|
1994
2312
|
if (this.closed) return;
|
|
@@ -2038,15 +2356,19 @@ var DocxodusEditor = (() => {
|
|
|
2038
2356
|
/**
|
|
2039
2357
|
* Repaint after an edit to `block` that would otherwise remount the whole document: a band
|
|
2040
2358
|
* repaints only itself (a story is one to three paragraphs), leaving the body DOM — and the
|
|
2041
|
-
* user's place in it — untouched.
|
|
2359
|
+
* user's place in it — untouched; a body edit reconciles incrementally. `forceRemount` is
|
|
2360
|
+
* for ops whose repaint provably needs whole-document context the reconciler cannot see:
|
|
2361
|
+
* list membership/level changes (sibling numbering shifts without sibling XML changing)
|
|
2362
|
+
* and border-div regrouping (HR insert, clearBorders).
|
|
2042
2363
|
*/
|
|
2043
|
-
refreshAfter(block, focusIndex, caretAtEnd = false) {
|
|
2364
|
+
refreshAfter(block, focusIndex, caretAtEnd = false, forceRemount = false) {
|
|
2044
2365
|
const band = this.region?.bandOf(block);
|
|
2045
2366
|
if (band) {
|
|
2046
2367
|
this.region.refresh(this.region.whichOf(band));
|
|
2047
2368
|
return;
|
|
2048
2369
|
}
|
|
2049
|
-
this.remount(focusIndex, caretAtEnd);
|
|
2370
|
+
if (forceRemount) this.remount(focusIndex, caretAtEnd);
|
|
2371
|
+
else this.reconcile(focusIndex, caretAtEnd);
|
|
2050
2372
|
}
|
|
2051
2373
|
/** Open a document, render it into `container`, and wire up editing. */
|
|
2052
2374
|
static open(container, bytes, exports, options = {}) {
|
|
@@ -2059,7 +2381,7 @@ var DocxodusEditor = (() => {
|
|
|
2059
2381
|
headerFooter: options.headerFooter ?? false,
|
|
2060
2382
|
onEdit: options.onEdit
|
|
2061
2383
|
};
|
|
2062
|
-
const handle = exports.DocxSessionBridge.OpenSession(bytes, '{"
|
|
2384
|
+
const handle = exports.DocxSessionBridge.OpenSession(bytes, '{"emitMarkdownPatch":false}');
|
|
2063
2385
|
const editor = new _DocxEditor(container, exports, handle, opts);
|
|
2064
2386
|
editor.refreshAnchorMap();
|
|
2065
2387
|
if (opts.headerFooter) editor.createRegion();
|
|
@@ -2123,7 +2445,9 @@ var DocxodusEditor = (() => {
|
|
|
2123
2445
|
* edit into a header part.
|
|
2124
2446
|
*/
|
|
2125
2447
|
refreshAnchorMap() {
|
|
2126
|
-
const
|
|
2448
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
2449
|
+
const raw = typeof bridge.ListAnchors === "function" ? bridge.ListAnchors(this.handle) : bridge.Project(this.handle);
|
|
2450
|
+
const proj = JSON.parse(raw);
|
|
2127
2451
|
this.unidToFullId.clear();
|
|
2128
2452
|
const bodyOwned = /* @__PURE__ */ new Set();
|
|
2129
2453
|
for (const [fullId, target] of Object.entries(proj.anchorIndex)) {
|
|
@@ -2188,6 +2512,7 @@ var DocxodusEditor = (() => {
|
|
|
2188
2512
|
this.container.innerHTML = styles + parsed.body.innerHTML;
|
|
2189
2513
|
this.editRoot = this.container;
|
|
2190
2514
|
if (this.options.editable) this.wireBlocks(this.container);
|
|
2515
|
+
this.stampPlanState();
|
|
2191
2516
|
return;
|
|
2192
2517
|
}
|
|
2193
2518
|
this.container.innerHTML = styles;
|
|
@@ -2197,6 +2522,7 @@ var DocxodusEditor = (() => {
|
|
|
2197
2522
|
this.container.appendChild(flow);
|
|
2198
2523
|
this.editRoot = flow;
|
|
2199
2524
|
if (this.options.editable) this.wireBlocks(flow);
|
|
2525
|
+
this.stampPlanState();
|
|
2200
2526
|
this.dockBands(flow);
|
|
2201
2527
|
}
|
|
2202
2528
|
/** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
|
|
@@ -2227,7 +2553,7 @@ var DocxodusEditor = (() => {
|
|
|
2227
2553
|
const unid = el.getAttribute("data-anchor");
|
|
2228
2554
|
if (!unid || !this.anchorIdOf(el)) return;
|
|
2229
2555
|
el.setAttribute("contenteditable", "true");
|
|
2230
|
-
el.querySelectorAll(
|
|
2556
|
+
el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
|
|
2231
2557
|
el.dataset.committedText = blockContentText(el);
|
|
2232
2558
|
el.addEventListener("focus", () => {
|
|
2233
2559
|
this.activeBlock = el;
|
|
@@ -2303,6 +2629,7 @@ var DocxodusEditor = (() => {
|
|
|
2303
2629
|
}
|
|
2304
2630
|
this.wireBlock(fresh);
|
|
2305
2631
|
if (this.activeBlock === el) this.activeBlock = fresh;
|
|
2632
|
+
this.maybeRenumberNotes(fresh);
|
|
2306
2633
|
}
|
|
2307
2634
|
}
|
|
2308
2635
|
this.options.onEdit?.({ anchorId: newAnchor, unid: newUnid });
|
|
@@ -2797,7 +3124,13 @@ var DocxodusEditor = (() => {
|
|
|
2797
3124
|
)
|
|
2798
3125
|
);
|
|
2799
3126
|
if (!res.success) return;
|
|
2800
|
-
this.refreshAfter(
|
|
3127
|
+
this.refreshAfter(
|
|
3128
|
+
block,
|
|
3129
|
+
idx,
|
|
3130
|
+
false,
|
|
3131
|
+
/* forceRemount */
|
|
3132
|
+
true
|
|
3133
|
+
);
|
|
2801
3134
|
}
|
|
2802
3135
|
/**
|
|
2803
3136
|
* Insert a `rows`×`cols` table after the active block. `options.cellContents` (row-major
|
|
@@ -2887,7 +3220,13 @@ var DocxodusEditor = (() => {
|
|
|
2887
3220
|
fullId = this.syncBlock(block, fullId);
|
|
2888
3221
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetListLevel(this.handle, fullId, delta));
|
|
2889
3222
|
if (!res.success) return;
|
|
2890
|
-
this.refreshAfter(
|
|
3223
|
+
this.refreshAfter(
|
|
3224
|
+
block,
|
|
3225
|
+
idx,
|
|
3226
|
+
false,
|
|
3227
|
+
/* forceRemount */
|
|
3228
|
+
true
|
|
3229
|
+
);
|
|
2891
3230
|
}
|
|
2892
3231
|
/** Toggle (or set) page-break-before on the active block. */
|
|
2893
3232
|
pageBreakBefore(value = true) {
|
|
@@ -2916,7 +3255,13 @@ var DocxodusEditor = (() => {
|
|
|
2916
3255
|
this.exports.DocxSessionBridge.ApplyListFormat(this.handle, fullId, isThisKind ? "none" : kind)
|
|
2917
3256
|
);
|
|
2918
3257
|
if (!res.success) return;
|
|
2919
|
-
this.refreshAfter(
|
|
3258
|
+
this.refreshAfter(
|
|
3259
|
+
block,
|
|
3260
|
+
idx,
|
|
3261
|
+
false,
|
|
3262
|
+
/* forceRemount */
|
|
3263
|
+
true
|
|
3264
|
+
);
|
|
2920
3265
|
}
|
|
2921
3266
|
/** Clear all paragraph borders (e.g. remove an inserted horizontal rule) on the active block —
|
|
2922
3267
|
* or every block in a multi-block selection. The engine/wire already accept `clearBorders`;
|
|
@@ -2945,6 +3290,40 @@ var DocxodusEditor = (() => {
|
|
|
2945
3290
|
if (!res.success) return;
|
|
2946
3291
|
this.refreshAfter(block, Math.max(0, idx - 1), true);
|
|
2947
3292
|
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Cite a new footnote from the caret position in the active body block. The note definition is
|
|
3295
|
+
* created (writing the whole Word scaffold — part, reserved separator notes, settings
|
|
3296
|
+
* declaration, styles — on a document that has none yet) and its body renders as ordinary
|
|
3297
|
+
* editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
|
|
3298
|
+
*
|
|
3299
|
+
* Body blocks only: Word disallows a note reference inside a header/footer story or inside
|
|
3300
|
+
* another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new
|
|
3301
|
+
* note renumbers the citations after it and can add a whole part.
|
|
3302
|
+
*/
|
|
3303
|
+
insertFootnote(markdown = "New footnote.") {
|
|
3304
|
+
this.insertNote("footnote", markdown);
|
|
3305
|
+
}
|
|
3306
|
+
/** Cite a new endnote from the caret — see {@link insertFootnote}; writes the endnotes part. */
|
|
3307
|
+
insertEndnote(markdown = "New endnote.") {
|
|
3308
|
+
this.insertNote("endnote", markdown);
|
|
3309
|
+
}
|
|
3310
|
+
insertNote(kind, markdown) {
|
|
3311
|
+
const block = this.activeBlock;
|
|
3312
|
+
if (this.closed || !block) return;
|
|
3313
|
+
if (this.isBandBlock(block) || block.closest(".footnotes, .endnotes")) return;
|
|
3314
|
+
let fullId = this.anchorIdOf(block);
|
|
3315
|
+
if (!fullId) return;
|
|
3316
|
+
const idx = this.blockIndex(block);
|
|
3317
|
+
const raw = caretOffsetIn(block);
|
|
3318
|
+
fullId = this.syncBlock(block, fullId);
|
|
3319
|
+
const offset = trimmedSplitOffset(block, raw ?? (block.textContent ?? "").length);
|
|
3320
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
3321
|
+
const call = kind === "footnote" ? bridge.InsertFootnote : bridge.InsertEndnote;
|
|
3322
|
+
if (!call) return;
|
|
3323
|
+
const res = this.parseEdit(call.call(bridge, this.handle, fullId, offset, markdown));
|
|
3324
|
+
if (!res.success) return;
|
|
3325
|
+
this.reconcile(idx, false);
|
|
3326
|
+
}
|
|
2948
3327
|
applyParagraphFormat(op) {
|
|
2949
3328
|
const block = this.activeBlock;
|
|
2950
3329
|
if (this.closed || !block) return;
|
|
@@ -2967,7 +3346,7 @@ var DocxodusEditor = (() => {
|
|
|
2967
3346
|
);
|
|
2968
3347
|
if (!res.success) return;
|
|
2969
3348
|
if (this.affectsList(res) || op.clearBorders) {
|
|
2970
|
-
this.refreshAfter(block, idx, false);
|
|
3349
|
+
this.refreshAfter(block, idx, false, true);
|
|
2971
3350
|
return;
|
|
2972
3351
|
}
|
|
2973
3352
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
@@ -2992,20 +3371,20 @@ var DocxodusEditor = (() => {
|
|
|
2992
3371
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetParagraphStyle(this.handle, fullId, styleId));
|
|
2993
3372
|
if (!res.success) return;
|
|
2994
3373
|
if (this.affectsList(res)) {
|
|
2995
|
-
this.refreshAfter(block, idx, false);
|
|
3374
|
+
this.refreshAfter(block, idx, false, true);
|
|
2996
3375
|
return;
|
|
2997
3376
|
}
|
|
2998
3377
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
2999
3378
|
}
|
|
3000
|
-
/** Undo the last edit (re-
|
|
3379
|
+
/** Undo the last edit (incremental repaint; falls back to a full re-render). */
|
|
3001
3380
|
undo() {
|
|
3002
3381
|
if (this.closed) return;
|
|
3003
|
-
if (this.exports.DocxSessionBridge.Undo(this.handle)) this.
|
|
3382
|
+
if (this.exports.DocxSessionBridge.Undo(this.handle)) this.reconcile();
|
|
3004
3383
|
}
|
|
3005
|
-
/** Redo the last undone edit (re-
|
|
3384
|
+
/** Redo the last undone edit (incremental repaint; falls back to a full re-render). */
|
|
3006
3385
|
redo() {
|
|
3007
3386
|
if (this.closed) return;
|
|
3008
|
-
if (this.exports.DocxSessionBridge.Redo(this.handle)) this.
|
|
3387
|
+
if (this.exports.DocxSessionBridge.Redo(this.handle)) this.reconcile();
|
|
3009
3388
|
}
|
|
3010
3389
|
// ─── Header/footer region commands (no-ops unless `headerFooter` is on) ───────────────
|
|
3011
3390
|
/**
|
|
@@ -3038,6 +3417,31 @@ var DocxodusEditor = (() => {
|
|
|
3038
3417
|
}
|
|
3039
3418
|
this.region.insertPageNumberInBand("footer", field);
|
|
3040
3419
|
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Set the page numbering of the section the bands describe (`w:pgNumType`) — Word's *Format Page
|
|
3422
|
+
* Numbers…*: `start` restarts numbering at that number, `format` chooses `1, 2, 3` vs
|
|
3423
|
+
* `i, ii, iii` etc. Omitted fields are left unchanged. Requires the header/footer region
|
|
3424
|
+
* (`{ headerFooter: true }`); a no-op otherwise.
|
|
3425
|
+
*
|
|
3426
|
+
* Inserted page-number fields are plain, so they render through this. The editor's own view still
|
|
3427
|
+
* shows each field's cached result — Word recomputes on open — but `{ paginated: true }`
|
|
3428
|
+
* substitutes the real per-page number and so reflects the change immediately.
|
|
3429
|
+
*/
|
|
3430
|
+
setPageNumbering(op) {
|
|
3431
|
+
this.assertOpen();
|
|
3432
|
+
this.region?.setPageNumbering(op);
|
|
3433
|
+
}
|
|
3434
|
+
/** Remove the section's page-numbering start/format: it reverts to continuing the previous
|
|
3435
|
+
* section's numbering in Word's default `1, 2, 3`. */
|
|
3436
|
+
clearPageNumbering() {
|
|
3437
|
+
this.assertOpen();
|
|
3438
|
+
this.region?.clearPageNumbering();
|
|
3439
|
+
}
|
|
3440
|
+
/** This section's page numbering as the document currently states it — `{}` when the section
|
|
3441
|
+
* sets neither (it continues the previous section in the default format). */
|
|
3442
|
+
pageNumbering() {
|
|
3443
|
+
return this.region?.pageNumbering() ?? {};
|
|
3444
|
+
}
|
|
3041
3445
|
/** Which inline formats the current selection carries — for ribbon button highlighting. */
|
|
3042
3446
|
queryFormatState() {
|
|
3043
3447
|
const block = this.activeBlock ?? this.editRoot;
|
|
@@ -3066,6 +3470,7 @@ var DocxodusEditor = (() => {
|
|
|
3066
3470
|
if (inBand) this.region.adoptBlock(fresh, anchorId);
|
|
3067
3471
|
this.wireBlock(fresh);
|
|
3068
3472
|
this.activeBlock = fresh;
|
|
3473
|
+
this.maybeRenumberNotes(fresh);
|
|
3069
3474
|
this.options.onEdit?.({ anchorId, unid: newUnid });
|
|
3070
3475
|
return fresh;
|
|
3071
3476
|
}
|
|
@@ -3088,7 +3493,7 @@ var DocxodusEditor = (() => {
|
|
|
3088
3493
|
);
|
|
3089
3494
|
if (html.charCodeAt(0) !== 123) return html;
|
|
3090
3495
|
}
|
|
3091
|
-
const bytes = bridge.Save(this.handle);
|
|
3496
|
+
const bytes = typeof bridge.SaveWithAnchorIds === "function" ? bridge.SaveWithAnchorIds(this.handle) : bridge.Save(this.handle);
|
|
3092
3497
|
return this.exports.DocumentConverter.ConvertDocxToHtmlComplete(
|
|
3093
3498
|
...completeArgs(bytes, this.options.cssPrefix, this.options.fabricateClasses, this.options.paginated, this.options.scale)
|
|
3094
3499
|
);
|
|
@@ -3110,6 +3515,358 @@ var DocxodusEditor = (() => {
|
|
|
3110
3515
|
affectsList(res) {
|
|
3111
3516
|
return [...res.modified ?? [], ...res.created ?? []].some((r) => r.kind === "li");
|
|
3112
3517
|
}
|
|
3518
|
+
// ─── Incremental structural reconcile ─────────────────────────────────
|
|
3519
|
+
//
|
|
3520
|
+
// After a structural op (insert table/row/col, footnote, delete block, undo/redo)
|
|
3521
|
+
// the DOM is patched from a unit-sequence diff against the session's render plan
|
|
3522
|
+
// instead of remounting the whole document (~3 s of full-document conversion on a
|
|
3523
|
+
// 350-block file). Full remount remains the universal FALLBACK: any ambiguity,
|
|
3524
|
+
// unsupported bridge, paginated mode, list-membership change, or thrown error
|
|
3525
|
+
// lands there — correctness never depends on the diff being right.
|
|
3526
|
+
/** True when the bridge carries the reconcile trio and the mode allows patching. */
|
|
3527
|
+
canReconcile() {
|
|
3528
|
+
const b = this.exports.DocxSessionBridge;
|
|
3529
|
+
return !this.options.paginated && typeof b.ListBlocks === "function" && typeof b.RenderBlocksHtml === "function" && typeof b.ListNotes === "function";
|
|
3530
|
+
}
|
|
3531
|
+
/** The body's top-level unit nodes in document order: `[data-anchor]` elements not
|
|
3532
|
+
* nested in another unit (cell paragraphs collapse into their table) and not in the
|
|
3533
|
+
* notes sections. */
|
|
3534
|
+
bodyUnitNodes() {
|
|
3535
|
+
const all = Array.from(this.editRoot.querySelectorAll("[data-anchor]"));
|
|
3536
|
+
return all.filter((el) => {
|
|
3537
|
+
if (el.closest("section.footnotes, section.endnotes")) return false;
|
|
3538
|
+
const ancestor = el.parentElement?.closest("[data-anchor]");
|
|
3539
|
+
return !(ancestor && this.editRoot.contains(ancestor));
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
/** The DOM diff token for a body unit node (see editor-reconcile.tokenOf). */
|
|
3543
|
+
static domTokenOf(el) {
|
|
3544
|
+
const unid = el.getAttribute("data-anchor") ?? "";
|
|
3545
|
+
const sig = el.getAttribute("data-render-sig");
|
|
3546
|
+
return sig ? `${unid}|${sig}` : unid;
|
|
3547
|
+
}
|
|
3548
|
+
/** The kind a body unit node would have in the plan (only 'li'/'tbl' matter to the
|
|
3549
|
+
* remount guard). */
|
|
3550
|
+
static domKindOf(el) {
|
|
3551
|
+
if (el.tagName === "TABLE") return "tbl";
|
|
3552
|
+
return el.querySelector(":scope > [data-list-marker]") ? "li" : "p";
|
|
3553
|
+
}
|
|
3554
|
+
static listMarkerText(el) {
|
|
3555
|
+
const m = el?.querySelector(":scope > [data-list-marker]");
|
|
3556
|
+
return m ? m.textContent : null;
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Incrementally patch the DOM from the session's render plan; falls back to
|
|
3560
|
+
* {@link remount} whenever it cannot prove the patch correct. Same focus contract
|
|
3561
|
+
* as remount.
|
|
3562
|
+
*/
|
|
3563
|
+
reconcile(focusIndex = -1, caretAtEnd = false) {
|
|
3564
|
+
if (!this.canReconcile()) {
|
|
3565
|
+
this.remount(focusIndex, caretAtEnd);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
try {
|
|
3569
|
+
if (!this.reconcileCore()) {
|
|
3570
|
+
this.remount(focusIndex, caretAtEnd);
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
} catch (err) {
|
|
3574
|
+
this.lastReconcileFallback = `threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
3575
|
+
this.remount(focusIndex, caretAtEnd);
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
if (focusIndex >= 0) {
|
|
3579
|
+
const blocks = this.editableList();
|
|
3580
|
+
const target = blocks[Math.min(focusIndex, blocks.length - 1)];
|
|
3581
|
+
if (target) {
|
|
3582
|
+
this.activeBlock = target;
|
|
3583
|
+
placeCaretAtOffset(target, caretAtEnd ? (target.textContent ?? "").length : 0);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
this.syncRegionToBody(this.activeBlock ?? void 0);
|
|
3587
|
+
}
|
|
3588
|
+
/** The patch itself. Returns false to request the remount fallback. */
|
|
3589
|
+
reconcileCore() {
|
|
3590
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
3591
|
+
this.refreshAnchorMap();
|
|
3592
|
+
const plan = JSON.parse(bridge.ListBlocks(this.handle));
|
|
3593
|
+
if (plan.error) return this.bail(`plan error: ${plan.error}`);
|
|
3594
|
+
const oldNodes = this.bodyUnitNodes();
|
|
3595
|
+
const oldTokens = oldNodes.map(_DocxEditor.domTokenOf);
|
|
3596
|
+
const oldKinds = oldNodes.map(_DocxEditor.domKindOf);
|
|
3597
|
+
const bodyDiff = diffUnits(oldTokens, plan.body);
|
|
3598
|
+
if (needsRemount(bodyDiff, plan.body, oldKinds)) return this.bail("needsRemount (li change or churn)");
|
|
3599
|
+
const fnState = this.notesDiff("footnotes", plan.footnotes);
|
|
3600
|
+
const enState = this.notesDiff("endnotes", plan.endnotes);
|
|
3601
|
+
if (fnState === null || enState === null) return this.bail("notes container unstampable/missing");
|
|
3602
|
+
const addedBodyIds = bodyDiff.added.map((j) => plan.body[j].id);
|
|
3603
|
+
const addedNoteIds = fnState.diff.added.map((j) => plan.footnotes[j].id).concat(enState.diff.added.map((j) => plan.endnotes[j].id));
|
|
3604
|
+
const allIds = addedBodyIds.concat(addedNoteIds);
|
|
3605
|
+
let rendered = {};
|
|
3606
|
+
if (allIds.length > 0) {
|
|
3607
|
+
rendered = JSON.parse(
|
|
3608
|
+
bridge.RenderBlocksHtml(
|
|
3609
|
+
this.handle,
|
|
3610
|
+
JSON.stringify(allIds),
|
|
3611
|
+
this.options.cssPrefix,
|
|
3612
|
+
this.options.fabricateClasses
|
|
3613
|
+
)
|
|
3614
|
+
);
|
|
3615
|
+
if (rendered.error) return this.bail(`render error: ${rendered.error}`);
|
|
3616
|
+
for (const id of allIds) if (!rendered[id]) return this.bail(`unrenderable: ${id}`);
|
|
3617
|
+
}
|
|
3618
|
+
const parse = (html) => {
|
|
3619
|
+
const el = new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
3620
|
+
el?.removeAttribute("xmlns");
|
|
3621
|
+
return el;
|
|
3622
|
+
};
|
|
3623
|
+
const freshBody = /* @__PURE__ */ new Map();
|
|
3624
|
+
for (const j of bodyDiff.added) {
|
|
3625
|
+
const el = parse(rendered[plan.body[j].id]);
|
|
3626
|
+
if (!el) return this.bail(`unparseable render: ${plan.body[j].id}`);
|
|
3627
|
+
freshBody.set(j, el);
|
|
3628
|
+
}
|
|
3629
|
+
for (const { oldIndex, newIndex } of bodyDiff.substituted) {
|
|
3630
|
+
const freshRoot = freshBody.get(newIndex);
|
|
3631
|
+
const oldMarker = _DocxEditor.listMarkerText(oldNodes[oldIndex]);
|
|
3632
|
+
const newMarker = _DocxEditor.listMarkerText(
|
|
3633
|
+
freshRoot ? _DocxEditor.anchorElOf(freshRoot) : null
|
|
3634
|
+
);
|
|
3635
|
+
if (oldMarker !== newMarker) return this.bail("substituted li marker drift");
|
|
3636
|
+
}
|
|
3637
|
+
if (!this.applyBodyDiff(oldNodes, plan.body, bodyDiff, freshBody)) return this.bail("applyBodyDiff bail");
|
|
3638
|
+
this.applyNotesDiff("footnotes", plan.footnotes, fnState, rendered);
|
|
3639
|
+
this.applyNotesDiff("endnotes", plan.endnotes, enState, rendered);
|
|
3640
|
+
const freshHasMarker = [...freshBody.values()].some(
|
|
3641
|
+
(el) => el.querySelector("a.footnote-ref, a.endnote-ref")
|
|
3642
|
+
);
|
|
3643
|
+
if (fnState.diff.added.length + fnState.diff.removed.length > 0 || freshHasMarker)
|
|
3644
|
+
this.renumberNoteChrome("footnote");
|
|
3645
|
+
if (enState.diff.added.length + enState.diff.removed.length > 0 || freshHasMarker)
|
|
3646
|
+
this.renumberNoteChrome("endnote");
|
|
3647
|
+
this.lastReconcileFallback = null;
|
|
3648
|
+
return true;
|
|
3649
|
+
}
|
|
3650
|
+
bail(reason) {
|
|
3651
|
+
this.lastReconcileFallback = reason;
|
|
3652
|
+
return false;
|
|
3653
|
+
}
|
|
3654
|
+
/** The generated single-child wrapper chain around a unit node (a table's alignment
|
|
3655
|
+
* <div>). Climbs while the parent is an anchor-less DIV whose ONLY element child is
|
|
3656
|
+
* the current node — never a section div (multi-child) or the edit root. */
|
|
3657
|
+
unitWrapperOf(el) {
|
|
3658
|
+
let n = el;
|
|
3659
|
+
while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
|
|
3660
|
+
n = n.parentElement;
|
|
3661
|
+
}
|
|
3662
|
+
return n;
|
|
3663
|
+
}
|
|
3664
|
+
/** The `[data-anchor]` element of a fresh render root (the root itself for a leaf
|
|
3665
|
+
* block, its descendant for a wrapper-shaped render like a table's align div). */
|
|
3666
|
+
static anchorElOf(root) {
|
|
3667
|
+
return root.hasAttribute("data-anchor") ? root : root.querySelector("[data-anchor]");
|
|
3668
|
+
}
|
|
3669
|
+
/** Insert/remove/swap body unit nodes per the diff. Returns false to bail (parent
|
|
3670
|
+
* ambiguity, order violation, wrapper semantics) — the session is already correct,
|
|
3671
|
+
* so bailing just means a full repaint. */
|
|
3672
|
+
applyBodyDiff(oldNodes, units, diff, fresh) {
|
|
3673
|
+
let lastOld = -1;
|
|
3674
|
+
for (let j = 0; j < units.length; j++) {
|
|
3675
|
+
const oi = diff.keep.get(j);
|
|
3676
|
+
if (oi === void 0) continue;
|
|
3677
|
+
if (oi < lastOld) return false;
|
|
3678
|
+
lastOld = oi;
|
|
3679
|
+
}
|
|
3680
|
+
const subOldByNew = new Map(diff.substituted.map((s) => [s.newIndex, s.oldIndex]));
|
|
3681
|
+
for (const [nj, oi] of subOldByNew) {
|
|
3682
|
+
const freshRoot = fresh.get(nj);
|
|
3683
|
+
const oldWrapper = this.unitWrapperOf(oldNodes[oi]);
|
|
3684
|
+
if (!freshRoot.hasAttribute("data-anchor")) {
|
|
3685
|
+
oldWrapper.replaceWith(freshRoot);
|
|
3686
|
+
} else if (oldWrapper === oldNodes[oi]) {
|
|
3687
|
+
oldNodes[oi].replaceWith(freshRoot);
|
|
3688
|
+
} else {
|
|
3689
|
+
return false;
|
|
3690
|
+
}
|
|
3691
|
+
this.wireUnit(freshRoot, units[nj]);
|
|
3692
|
+
}
|
|
3693
|
+
const pureAdded = diff.added.filter((j) => !subOldByNew.has(j));
|
|
3694
|
+
const pureRemoved = diff.removed.filter(
|
|
3695
|
+
(i) => !diff.substituted.some((s) => s.oldIndex === i)
|
|
3696
|
+
);
|
|
3697
|
+
const nodeAt = (j) => {
|
|
3698
|
+
const oi = diff.keep.get(j);
|
|
3699
|
+
if (oi !== void 0) return oldNodes[oi];
|
|
3700
|
+
if (subOldByNew.has(j)) return fresh.get(j);
|
|
3701
|
+
const f = fresh.get(j);
|
|
3702
|
+
return f && f.isConnected ? f : null;
|
|
3703
|
+
};
|
|
3704
|
+
for (const j of pureAdded) {
|
|
3705
|
+
const el = fresh.get(j);
|
|
3706
|
+
let prev = null;
|
|
3707
|
+
for (let k = j - 1; k >= 0 && !prev; k--) prev = nodeAt(k);
|
|
3708
|
+
let next = null;
|
|
3709
|
+
for (let k = j + 1; k < units.length && !next; k++) {
|
|
3710
|
+
const oi = diff.keep.get(k);
|
|
3711
|
+
if (oi !== void 0) next = oldNodes[oi];
|
|
3712
|
+
else if (subOldByNew.has(k)) next = fresh.get(k);
|
|
3713
|
+
}
|
|
3714
|
+
const prevW = prev ? this.unitWrapperOf(prev) : null;
|
|
3715
|
+
const nextW = next ? this.unitWrapperOf(next) : null;
|
|
3716
|
+
if (prevW && nextW && prevW.parentElement !== nextW.parentElement) return false;
|
|
3717
|
+
if (prevW) prevW.after(el);
|
|
3718
|
+
else if (nextW) nextW.before(el);
|
|
3719
|
+
else return false;
|
|
3720
|
+
this.wireUnit(el, units[j]);
|
|
3721
|
+
}
|
|
3722
|
+
for (const i of pureRemoved) {
|
|
3723
|
+
const wrapper = this.unitWrapperOf(oldNodes[i]);
|
|
3724
|
+
wrapper.remove();
|
|
3725
|
+
}
|
|
3726
|
+
return true;
|
|
3727
|
+
}
|
|
3728
|
+
/** Wire a freshly rendered unit root (and its nested blocks) and stamp the unit's
|
|
3729
|
+
* content signature on its `[data-anchor]` element — the element the next
|
|
3730
|
+
* reconcile's DOM walk reads tokens from. */
|
|
3731
|
+
wireUnit(root, unit) {
|
|
3732
|
+
const anchorEl = _DocxEditor.anchorElOf(root);
|
|
3733
|
+
if (anchorEl && unit.sig) anchorEl.setAttribute("data-render-sig", unit.sig);
|
|
3734
|
+
if (anchorEl) this.wireBlock(anchorEl);
|
|
3735
|
+
root.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
|
|
3736
|
+
}
|
|
3737
|
+
/** Old-sequence diff state for one notes section. `null` requests remount (DOM not
|
|
3738
|
+
* stampable/consistent). */
|
|
3739
|
+
notesDiff(sectionClass, units) {
|
|
3740
|
+
const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
|
|
3741
|
+
const lis = ol ? Array.from(ol.children).filter((c) => c.tagName === "LI") : [];
|
|
3742
|
+
if (units.length === 0 && lis.length === 0) return { lis, diff: diffUnits([], []) };
|
|
3743
|
+
if (!ol) return null;
|
|
3744
|
+
const tokens = [];
|
|
3745
|
+
for (const li of lis) {
|
|
3746
|
+
const unid = li.getAttribute("data-note-anchor");
|
|
3747
|
+
if (!unid) return null;
|
|
3748
|
+
const sig = li.getAttribute("data-render-sig");
|
|
3749
|
+
tokens.push(sig ? `${unid}|${sig}` : unid);
|
|
3750
|
+
}
|
|
3751
|
+
return { lis, diff: diffUnits(tokens, units) };
|
|
3752
|
+
}
|
|
3753
|
+
/** Apply a notes-section diff: rebuild the `<ol>`'s li list, preserving kept nodes. */
|
|
3754
|
+
applyNotesDiff(sectionClass, units, state, rendered) {
|
|
3755
|
+
if (state.diff.added.length === 0 && state.diff.removed.length === 0) return;
|
|
3756
|
+
if (units.length === 0) {
|
|
3757
|
+
this.editRoot.querySelector(`section.${sectionClass}`)?.remove();
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
|
|
3761
|
+
const prefix = sectionClass === "footnotes" ? "fn" : "en";
|
|
3762
|
+
const nodes = [];
|
|
3763
|
+
for (let j = 0; j < units.length; j++) {
|
|
3764
|
+
const oi = state.diff.keep.get(j);
|
|
3765
|
+
if (oi !== void 0) {
|
|
3766
|
+
nodes.push(state.lis[oi]);
|
|
3767
|
+
continue;
|
|
3768
|
+
}
|
|
3769
|
+
nodes.push(this.buildNoteLi(prefix, units[j], rendered[units[j].id]));
|
|
3770
|
+
}
|
|
3771
|
+
ol.replaceChildren(...nodes);
|
|
3772
|
+
}
|
|
3773
|
+
/** Build a notes-section `<li>` for a freshly rendered note — replicating the
|
|
3774
|
+
* converter's chrome (id/value are re-stamped by the renumber pass; the backref
|
|
3775
|
+
* goes inside the last paragraph, matching RenderFootnoteItem). */
|
|
3776
|
+
buildNoteLi(prefix, unit, html) {
|
|
3777
|
+
const li = document.createElement("li");
|
|
3778
|
+
li.setAttribute("data-note-anchor", unidOf(unit.id));
|
|
3779
|
+
if (unit.sig) li.setAttribute("data-render-sig", unit.sig);
|
|
3780
|
+
li.innerHTML = html;
|
|
3781
|
+
const paras = li.querySelectorAll(":scope > p");
|
|
3782
|
+
const last = paras[paras.length - 1];
|
|
3783
|
+
if (last) {
|
|
3784
|
+
const backref = document.createElement("a");
|
|
3785
|
+
backref.setAttribute("class", `${prefix}-backref`);
|
|
3786
|
+
backref.setAttribute("contenteditable", "false");
|
|
3787
|
+
backref.textContent = "\u21A9";
|
|
3788
|
+
last.append(" ", backref);
|
|
3789
|
+
}
|
|
3790
|
+
li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
|
|
3791
|
+
return li;
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* Rewrite position-derived note chrome from the session's citation-ordered note
|
|
3795
|
+
* list: the k-th marker in document order IS note k (ids ascend in reference
|
|
3796
|
+
* order), so marker sup text, hrefs/ids, li ids/values and backref hrefs are all
|
|
3797
|
+
* re-derived positionally. Pure attribute/text patching of generated chrome.
|
|
3798
|
+
*/
|
|
3799
|
+
renumberNoteChrome(kind) {
|
|
3800
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
3801
|
+
if (typeof bridge.ListNotes !== "function") return;
|
|
3802
|
+
const prefix = kind === "footnote" ? "fn" : "en";
|
|
3803
|
+
let notes;
|
|
3804
|
+
try {
|
|
3805
|
+
notes = JSON.parse(bridge.ListNotes(this.handle, kind === "endnote"));
|
|
3806
|
+
} catch {
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
if (!Array.isArray(notes)) return;
|
|
3810
|
+
const markers = Array.from(
|
|
3811
|
+
this.editRoot.querySelectorAll(`a.${kind}-ref`)
|
|
3812
|
+
).filter((a) => !a.closest("section.footnotes, section.endnotes"));
|
|
3813
|
+
markers.forEach((a, k) => {
|
|
3814
|
+
const n = notes[k];
|
|
3815
|
+
if (!n) return;
|
|
3816
|
+
a.setAttribute("href", `#${prefix}-${n.id}`);
|
|
3817
|
+
a.id = `${prefix}-ref-${n.id}`;
|
|
3818
|
+
if (kind === "footnote") a.setAttribute("data-footnote-id", n.id);
|
|
3819
|
+
const sup = a.querySelector("sup");
|
|
3820
|
+
if (sup) sup.textContent = String(n.ordinal);
|
|
3821
|
+
});
|
|
3822
|
+
const byUnid = new Map(notes.map((n) => [unidOf(n.defAnchorId), n]));
|
|
3823
|
+
const lis = this.editRoot.querySelectorAll(`section.${kind}s > ol > li`);
|
|
3824
|
+
lis.forEach((li) => {
|
|
3825
|
+
const unid = li.getAttribute("data-note-anchor");
|
|
3826
|
+
const n = unid ? byUnid.get(unid) : void 0;
|
|
3827
|
+
if (!n) return;
|
|
3828
|
+
li.id = `${prefix}-${n.id}`;
|
|
3829
|
+
li.setAttribute("value", String(n.ordinal));
|
|
3830
|
+
li.querySelectorAll(`a.${prefix}-backref`).forEach(
|
|
3831
|
+
(b) => b.setAttribute("href", `#${prefix}-ref-${n.id}`)
|
|
3832
|
+
);
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
/** After an incremental block swap, stale marker chrome in the swapped node (the
|
|
3836
|
+
* throwaway render numbers citations from 1) is repaired in place. */
|
|
3837
|
+
maybeRenumberNotes(fresh) {
|
|
3838
|
+
if (fresh.querySelector("a.footnote-ref")) this.renumberNoteChrome("footnote");
|
|
3839
|
+
if (fresh.querySelector("a.endnote-ref")) this.renumberNoteChrome("endnote");
|
|
3840
|
+
}
|
|
3841
|
+
/** Stamp the DOM state the reconciler diffs against: container signatures on body
|
|
3842
|
+
* tables and `data-note-anchor` + signature on notes-section items. Called after
|
|
3843
|
+
* every full mount; reconcile stamps its own insertions. */
|
|
3844
|
+
stampPlanState() {
|
|
3845
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
3846
|
+
if (typeof bridge.ListBlocks !== "function") return;
|
|
3847
|
+
try {
|
|
3848
|
+
const plan = JSON.parse(bridge.ListBlocks(this.handle));
|
|
3849
|
+
if (plan.error) return;
|
|
3850
|
+
const nodes = this.bodyUnitNodes();
|
|
3851
|
+
if (nodes.length === plan.body.length) {
|
|
3852
|
+
nodes.forEach((el, k) => {
|
|
3853
|
+
if (plan.body[k].sig && el.getAttribute("data-anchor") === unidOf(plan.body[k].id))
|
|
3854
|
+
el.setAttribute("data-render-sig", plan.body[k].sig);
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
const stampNotes = (sectionClass, units) => {
|
|
3858
|
+
const lis = this.editRoot.querySelectorAll(`section.${sectionClass} > ol > li`);
|
|
3859
|
+
if (lis.length !== units.length) return;
|
|
3860
|
+
lis.forEach((li, k) => {
|
|
3861
|
+
li.setAttribute("data-note-anchor", unidOf(units[k].id));
|
|
3862
|
+
if (units[k].sig) li.setAttribute("data-render-sig", units[k].sig);
|
|
3863
|
+
});
|
|
3864
|
+
};
|
|
3865
|
+
stampNotes("footnotes", plan.footnotes);
|
|
3866
|
+
stampNotes("endnotes", plan.endnotes);
|
|
3867
|
+
} catch {
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3113
3870
|
/**
|
|
3114
3871
|
* Full re-render from current session state (after undo/redo, and after list edits where
|
|
3115
3872
|
* single-block rendering can't compute numbering). Optionally focus the editable block at
|