docxodus 7.1.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/docxodus.worker.js +2 -2
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor-headerfooter.d.ts +166 -0
- package/dist/editor-headerfooter.d.ts.map +1 -0
- package/dist/editor-headerfooter.js +530 -0
- package/dist/editor-headerfooter.js.map +1 -0
- 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 +1861 -110
- package/dist/editor.d.ts +200 -6
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +868 -86
- 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 +3 -4
- 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 +635 -37
- package/dist/pagination.d.ts +119 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +706 -52
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/react.js.map +1 -1
- package/dist/session.bundle.js +241 -4
- package/dist/session.d.ts +160 -6
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +202 -4
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +238 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -11
- 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.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encoding.Extensions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.Thread.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.ReaderWriter.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.wasm +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +41 -41
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.map +1 -1
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.js.symbols +2102 -2101
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.map +1 -1
- package/dist/wasm/index.html +2 -1
- 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) {
|
|
@@ -84,6 +120,7 @@ var DocxodusEditor = (() => {
|
|
|
84
120
|
this.cssPrefix = options.cssPrefix ?? "page-";
|
|
85
121
|
this.showPageNumbers = options.showPageNumbers ?? true;
|
|
86
122
|
this.pageGap = options.pageGap ?? 20;
|
|
123
|
+
this.fragmentParagraphs = options.fragmentParagraphs ?? false;
|
|
87
124
|
this.hfRegistry = /* @__PURE__ */ new Map();
|
|
88
125
|
this.footnoteRegistry = /* @__PURE__ */ new Map();
|
|
89
126
|
}
|
|
@@ -100,6 +137,7 @@ var DocxodusEditor = (() => {
|
|
|
100
137
|
const sections = this.stagingElement.querySelectorAll(
|
|
101
138
|
"[data-section-index]"
|
|
102
139
|
);
|
|
140
|
+
this.pageNumbering = this.parsePageNumbering(sections);
|
|
103
141
|
const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
|
|
104
142
|
for (const section of sectionsToProcess) {
|
|
105
143
|
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
@@ -115,8 +153,62 @@ var DocxodusEditor = (() => {
|
|
|
115
153
|
pageNumber += sectionPages.length;
|
|
116
154
|
}
|
|
117
155
|
this.stagingElement.style.display = "none";
|
|
156
|
+
this.substitutePageNumberFields(pages.length);
|
|
118
157
|
return { totalPages: pages.length, pages };
|
|
119
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
|
+
}
|
|
120
212
|
/**
|
|
121
213
|
* Measures all content blocks in a section.
|
|
122
214
|
*/
|
|
@@ -150,6 +242,393 @@ var DocxodusEditor = (() => {
|
|
|
150
242
|
}
|
|
151
243
|
return blocks;
|
|
152
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Measures one element in the same hidden staging context used for the source blocks.
|
|
247
|
+
* This is intentionally DOM-based: table row heights cannot be inferred from individual
|
|
248
|
+
* rows because wrapping and collapsed borders change the height of a fragment.
|
|
249
|
+
*/
|
|
250
|
+
measureElement(element, dims) {
|
|
251
|
+
const measurementHost = document.createElement("div");
|
|
252
|
+
measurementHost.style.position = "absolute";
|
|
253
|
+
measurementHost.style.visibility = "hidden";
|
|
254
|
+
measurementHost.style.left = "-9999px";
|
|
255
|
+
measurementHost.style.width = `${dims.contentWidth}pt`;
|
|
256
|
+
const measuredElement = element.cloneNode(true);
|
|
257
|
+
measurementHost.appendChild(measuredElement);
|
|
258
|
+
this.stagingElement.appendChild(measurementHost);
|
|
259
|
+
const rect = measuredElement.getBoundingClientRect();
|
|
260
|
+
const style = window.getComputedStyle(measuredElement);
|
|
261
|
+
const measured = {
|
|
262
|
+
element,
|
|
263
|
+
heightPt: pxToPt(rect.height),
|
|
264
|
+
marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
|
|
265
|
+
marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
|
|
266
|
+
keepWithNext: element.dataset.keepWithNext === "true",
|
|
267
|
+
keepLines: element.dataset.keepLines === "true",
|
|
268
|
+
pageBreakBefore: element.dataset.pageBreakBefore === "true",
|
|
269
|
+
isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
|
|
270
|
+
};
|
|
271
|
+
this.stagingElement.removeChild(measurementHost);
|
|
272
|
+
return measured;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Returns the contiguous keep-with-next chain beginning at a block.
|
|
276
|
+
*
|
|
277
|
+
* A hard page break or a page-break-before directive is stronger than a
|
|
278
|
+
* keep-with-next directive, so it terminates the chain. The caller only
|
|
279
|
+
* keeps a chain together when the whole chain can fit on a fresh page.
|
|
280
|
+
*/
|
|
281
|
+
getKeepWithNextChain(blocks, startIndex) {
|
|
282
|
+
const firstBlock = blocks[startIndex];
|
|
283
|
+
if (!firstBlock) return [];
|
|
284
|
+
const chain = [firstBlock];
|
|
285
|
+
let lastIndex = startIndex;
|
|
286
|
+
while (blocks[lastIndex].keepWithNext) {
|
|
287
|
+
const nextBlock = blocks[lastIndex + 1];
|
|
288
|
+
if (!nextBlock || nextBlock.isPageBreak || nextBlock.pageBreakBefore) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
chain.push(nextBlock);
|
|
292
|
+
lastIndex++;
|
|
293
|
+
}
|
|
294
|
+
return chain;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Measures the visible body height of a keep-with-next chain using the same
|
|
298
|
+
* collapsed-margin rules as normal block placement. The trailing margin is
|
|
299
|
+
* intentionally excluded, matching the individual block fit check.
|
|
300
|
+
*/
|
|
301
|
+
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
|
|
302
|
+
const firstBlock = chain[0];
|
|
303
|
+
if (!firstBlock) return 0;
|
|
304
|
+
const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
305
|
+
let bodyHeight = firstMarginTop + firstBlock.heightPt;
|
|
306
|
+
for (let index = 1; index < chain.length; index++) {
|
|
307
|
+
const previousBlock = chain[index - 1];
|
|
308
|
+
const block = chain[index];
|
|
309
|
+
bodyHeight += Math.max(previousBlock.marginBottomPt, block.marginTopPt) + block.heightPt;
|
|
310
|
+
}
|
|
311
|
+
return bodyHeight;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Finds footnote references introduced by a sequence of blocks, preserving
|
|
315
|
+
* document order and excluding references already assigned to the page.
|
|
316
|
+
*/
|
|
317
|
+
collectNewFootnoteIds(blocks, existingFootnoteIds) {
|
|
318
|
+
const knownIds = new Set(existingFootnoteIds);
|
|
319
|
+
const newIds = [];
|
|
320
|
+
for (const block of blocks) {
|
|
321
|
+
for (const id of this.extractFootnoteRefs(block.element)) {
|
|
322
|
+
if (!knownIds.has(id)) {
|
|
323
|
+
knownIds.add(id);
|
|
324
|
+
newIds.push(id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return newIds;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The shortest body available to a section's first, default, or even page.
|
|
332
|
+
* A row fragment must fit every variant, otherwise a later header/footer could
|
|
333
|
+
* send it through the oversized-block fallback again.
|
|
334
|
+
*/
|
|
335
|
+
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
336
|
+
return Math.min(
|
|
337
|
+
this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
|
|
338
|
+
this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
|
|
339
|
+
this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Builds a clone of a simple table wrapper containing a contiguous run of rows.
|
|
344
|
+
* Complex table features are deliberately rejected by the caller: a split across
|
|
345
|
+
* merged cells, nested tables, or footnotes cannot be made correct by cloning rows.
|
|
346
|
+
*/
|
|
347
|
+
createSimpleTableFragment(wrapper, table, body, rows, retainAnchor) {
|
|
348
|
+
const wrapperClone = wrapper.cloneNode(false);
|
|
349
|
+
const tableClone = table.cloneNode(false);
|
|
350
|
+
for (const child of Array.from(table.children)) {
|
|
351
|
+
if (child !== body) {
|
|
352
|
+
tableClone.appendChild(child.cloneNode(true));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const bodyClone = body.cloneNode(false);
|
|
356
|
+
for (const row of rows) {
|
|
357
|
+
bodyClone.appendChild(row.cloneNode(true));
|
|
358
|
+
}
|
|
359
|
+
tableClone.appendChild(bodyClone);
|
|
360
|
+
if (!retainAnchor) {
|
|
361
|
+
wrapperClone.removeAttribute("data-anchor");
|
|
362
|
+
tableClone.removeAttribute("data-anchor");
|
|
363
|
+
}
|
|
364
|
+
wrapperClone.appendChild(tableClone);
|
|
365
|
+
return wrapperClone;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Splits an oversized, ordinary table at row boundaries. This only participates
|
|
369
|
+
* in the existing oversized-block fallback; unsupported tables keep the previous
|
|
370
|
+
* overflow behavior rather than risking broken table semantics.
|
|
371
|
+
*/
|
|
372
|
+
trySplitSimpleOversizedTable(block, dims, sectionIndex) {
|
|
373
|
+
const wrapper = block.element;
|
|
374
|
+
if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const table = wrapper.firstElementChild;
|
|
378
|
+
if (!(table instanceof HTMLTableElement)) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
382
|
+
if (!body || table.tHead || table.tFoot || body.rows.length < 2 || Array.from(table.children).some((child) => child !== body && child.tagName !== "COLGROUP") || table.querySelector("table, [rowspan], [colspan], [data-footnote-id]") || wrapper.querySelector("[data-footnote-id]")) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const rows = Array.from(body.rows);
|
|
386
|
+
const minimumContentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
387
|
+
const maximumFragmentHeight = minimumContentHeight - block.marginTopPt - block.marginBottomPt;
|
|
388
|
+
if (maximumFragmentHeight <= 0) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const groups = [];
|
|
392
|
+
let start = 0;
|
|
393
|
+
while (start < rows.length) {
|
|
394
|
+
let end = start;
|
|
395
|
+
while (end < rows.length) {
|
|
396
|
+
const candidate = this.createSimpleTableFragment(
|
|
397
|
+
wrapper,
|
|
398
|
+
table,
|
|
399
|
+
body,
|
|
400
|
+
rows.slice(start, end + 1),
|
|
401
|
+
start === 0
|
|
402
|
+
);
|
|
403
|
+
const measured = this.measureElement(candidate, dims);
|
|
404
|
+
if (measured.heightPt > maximumFragmentHeight) {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
end++;
|
|
408
|
+
}
|
|
409
|
+
if (end === start) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
groups.push(rows.slice(start, end));
|
|
413
|
+
start = end;
|
|
414
|
+
}
|
|
415
|
+
if (groups.length < 2) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const fragments = [];
|
|
419
|
+
for (let index = 0; index < groups.length; index++) {
|
|
420
|
+
const isFirst = index === 0;
|
|
421
|
+
const isLast = index === groups.length - 1;
|
|
422
|
+
const fragment = this.createSimpleTableFragment(
|
|
423
|
+
wrapper,
|
|
424
|
+
table,
|
|
425
|
+
body,
|
|
426
|
+
groups[index],
|
|
427
|
+
isFirst
|
|
428
|
+
);
|
|
429
|
+
if (!isFirst) {
|
|
430
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
431
|
+
}
|
|
432
|
+
if (!isLast) {
|
|
433
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
434
|
+
}
|
|
435
|
+
const measured = this.measureElement(fragment, dims);
|
|
436
|
+
if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
fragments.push({
|
|
440
|
+
...measured,
|
|
441
|
+
keepWithNext: false,
|
|
442
|
+
keepLines: false,
|
|
443
|
+
pageBreakBefore: false,
|
|
444
|
+
isPageBreak: false
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return fragments;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
|
|
451
|
+
* after whitespace or at a run boundary so the paginator never deliberately
|
|
452
|
+
* cuts through an ordinary word merely to fill a little more of a page.
|
|
453
|
+
*/
|
|
454
|
+
paragraphFragmentEndpoints(paragraph) {
|
|
455
|
+
const endpoints = [];
|
|
456
|
+
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
|
|
457
|
+
let textNode;
|
|
458
|
+
while (textNode = walker.nextNode()) {
|
|
459
|
+
const text = textNode.data;
|
|
460
|
+
if (text.length === 0) continue;
|
|
461
|
+
const whitespace = /\s+/g;
|
|
462
|
+
let match;
|
|
463
|
+
while ((match = whitespace.exec(text)) !== null) {
|
|
464
|
+
endpoints.push({ node: textNode, offset: match.index + match[0].length });
|
|
465
|
+
}
|
|
466
|
+
if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
|
|
467
|
+
endpoints.push({ node: textNode, offset: text.length });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return endpoints;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Whether a range contains visible text after ignoring bidi/zero-width marks.
|
|
474
|
+
* Paragraph fragmentation deliberately excludes non-textual descendants, so
|
|
475
|
+
* this is enough to reject empty head or tail fragments.
|
|
476
|
+
*/
|
|
477
|
+
hasVisibleFragmentText(fragment) {
|
|
478
|
+
return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Phase one only handles text-like paragraph descendants. Objects, explicit
|
|
482
|
+
* line breaks, list markers, notes, and out-of-flow/inline-block content all
|
|
483
|
+
* require their own line-layout rules and retain the established whole-block
|
|
484
|
+
* fallback instead of risking broken content or duplicate anchors.
|
|
485
|
+
*/
|
|
486
|
+
canFragmentParagraph(block) {
|
|
487
|
+
if (!this.fragmentParagraphs || block.element.tagName !== "P") {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const paragraph = block.element;
|
|
491
|
+
if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
const unsupportedDescendants = [
|
|
495
|
+
"br",
|
|
496
|
+
"img",
|
|
497
|
+
"picture",
|
|
498
|
+
"svg",
|
|
499
|
+
"math",
|
|
500
|
+
"canvas",
|
|
501
|
+
"video",
|
|
502
|
+
"audio",
|
|
503
|
+
"iframe",
|
|
504
|
+
"object",
|
|
505
|
+
"embed",
|
|
506
|
+
"input",
|
|
507
|
+
"button",
|
|
508
|
+
"select",
|
|
509
|
+
"textarea",
|
|
510
|
+
"table",
|
|
511
|
+
"ol",
|
|
512
|
+
"ul",
|
|
513
|
+
"li",
|
|
514
|
+
"dl",
|
|
515
|
+
"div",
|
|
516
|
+
"p",
|
|
517
|
+
"section",
|
|
518
|
+
"article",
|
|
519
|
+
"aside",
|
|
520
|
+
"figure",
|
|
521
|
+
"fieldset",
|
|
522
|
+
"details",
|
|
523
|
+
"[data-footnote-id]",
|
|
524
|
+
"[data-list-marker]",
|
|
525
|
+
"[data-anchor]",
|
|
526
|
+
"[id]",
|
|
527
|
+
"[contenteditable]"
|
|
528
|
+
].join(", ");
|
|
529
|
+
if (paragraph.querySelector(unsupportedDescendants)) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
const paragraphStyle = window.getComputedStyle(paragraph);
|
|
533
|
+
if (paragraphStyle.display !== "block" || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
|
|
537
|
+
const style = window.getComputedStyle(descendant);
|
|
538
|
+
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Builds one range-cloned paragraph fragment. Only the leading fragment keeps
|
|
546
|
+
* the source paragraph's addressability; continuations must not duplicate an
|
|
547
|
+
* id/data-anchor in the rendered document.
|
|
548
|
+
*/
|
|
549
|
+
createParagraphFragment(paragraph, range, retainSourceIdentity, isFinalFragment) {
|
|
550
|
+
const fragment = paragraph.cloneNode(false);
|
|
551
|
+
const contents = range.cloneContents();
|
|
552
|
+
fragment.appendChild(contents);
|
|
553
|
+
if (!retainSourceIdentity) {
|
|
554
|
+
fragment.removeAttribute("id");
|
|
555
|
+
fragment.removeAttribute("data-anchor");
|
|
556
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
557
|
+
fragment.style.setProperty("text-indent", "0", "important");
|
|
558
|
+
}
|
|
559
|
+
if (!isFinalFragment) {
|
|
560
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
561
|
+
}
|
|
562
|
+
return fragment;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Splits a simple paragraph at the largest DOM Range endpoint that fits the
|
|
566
|
+
* currently available body space. The caller then processes the tail normally,
|
|
567
|
+
* allowing it to fragment again on later pages when necessary.
|
|
568
|
+
*/
|
|
569
|
+
tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
|
|
570
|
+
if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const paragraph = block.element;
|
|
574
|
+
const endpoints = this.paragraphFragmentEndpoints(paragraph);
|
|
575
|
+
if (endpoints.length < 2) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
let low = 0;
|
|
579
|
+
let high = endpoints.length - 2;
|
|
580
|
+
let best = null;
|
|
581
|
+
while (low <= high) {
|
|
582
|
+
const middle = Math.floor((low + high) / 2);
|
|
583
|
+
const endpoint = endpoints[middle];
|
|
584
|
+
const headRange = document.createRange();
|
|
585
|
+
headRange.setStart(paragraph, 0);
|
|
586
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
587
|
+
const headContents = headRange.cloneContents();
|
|
588
|
+
if (!this.hasVisibleFragmentText(headContents)) {
|
|
589
|
+
low = middle + 1;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const head = this.createParagraphFragment(paragraph, headRange, true, false);
|
|
593
|
+
const measured = this.measureElement(head, dims);
|
|
594
|
+
if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
|
|
595
|
+
best = { endpoint, element: head, measured };
|
|
596
|
+
low = middle + 1;
|
|
597
|
+
} else {
|
|
598
|
+
high = middle - 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!best) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const tailRange = document.createRange();
|
|
605
|
+
tailRange.setStart(best.endpoint.node, best.endpoint.offset);
|
|
606
|
+
tailRange.setEnd(paragraph, paragraph.childNodes.length);
|
|
607
|
+
const tailContents = tailRange.cloneContents();
|
|
608
|
+
if (!this.hasVisibleFragmentText(tailContents)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
|
|
612
|
+
const tailMeasured = this.measureElement(tail, dims);
|
|
613
|
+
return [
|
|
614
|
+
{
|
|
615
|
+
...best.measured,
|
|
616
|
+
element: best.element,
|
|
617
|
+
keepWithNext: false,
|
|
618
|
+
keepLines: false,
|
|
619
|
+
pageBreakBefore: false,
|
|
620
|
+
isPageBreak: false
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
...tailMeasured,
|
|
624
|
+
element: tail,
|
|
625
|
+
keepWithNext: false,
|
|
626
|
+
keepLines: false,
|
|
627
|
+
pageBreakBefore: false,
|
|
628
|
+
isPageBreak: false
|
|
629
|
+
}
|
|
630
|
+
];
|
|
631
|
+
}
|
|
153
632
|
/**
|
|
154
633
|
* Parses the header/footer registry from the staging element.
|
|
155
634
|
* Also measures heights during parsing for lazy-loading compatibility.
|
|
@@ -253,6 +732,7 @@ var DocxodusEditor = (() => {
|
|
|
253
732
|
measureContainer.style.visibility = "hidden";
|
|
254
733
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
255
734
|
measureContainer.style.left = "-9999px";
|
|
735
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
256
736
|
const hr = document.createElement("hr");
|
|
257
737
|
measureContainer.appendChild(hr);
|
|
258
738
|
if (hasContinuation) {
|
|
@@ -287,6 +767,7 @@ var DocxodusEditor = (() => {
|
|
|
287
767
|
measureContainer.style.visibility = "hidden";
|
|
288
768
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
289
769
|
measureContainer.style.left = "-9999px";
|
|
770
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
290
771
|
const hr = document.createElement("hr");
|
|
291
772
|
measureContainer.appendChild(hr);
|
|
292
773
|
for (const el of continuation.remainingElements) {
|
|
@@ -305,11 +786,17 @@ var DocxodusEditor = (() => {
|
|
|
305
786
|
splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth) {
|
|
306
787
|
const footnoteContent = footnoteElement.querySelector(".footnote-content");
|
|
307
788
|
if (!footnoteContent) {
|
|
308
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
fits: Array.from(footnoteElement.children).map((el) => el.cloneNode(true)),
|
|
791
|
+
overflow: []
|
|
792
|
+
};
|
|
309
793
|
}
|
|
310
794
|
const children = Array.from(footnoteContent.children);
|
|
311
795
|
if (children.length <= 1) {
|
|
312
|
-
return {
|
|
796
|
+
return {
|
|
797
|
+
fits: children.map((el) => el.cloneNode(true)),
|
|
798
|
+
overflow: []
|
|
799
|
+
};
|
|
313
800
|
}
|
|
314
801
|
const fits = [];
|
|
315
802
|
const overflow = [];
|
|
@@ -319,6 +806,7 @@ var DocxodusEditor = (() => {
|
|
|
319
806
|
hrMeasure.style.visibility = "hidden";
|
|
320
807
|
hrMeasure.style.width = `${contentWidth}pt`;
|
|
321
808
|
hrMeasure.style.left = "-9999px";
|
|
809
|
+
hrMeasure.className = this.cssPrefix + "footnotes";
|
|
322
810
|
const hr = document.createElement("hr");
|
|
323
811
|
hrMeasure.appendChild(hr);
|
|
324
812
|
this.stagingElement.appendChild(hrMeasure);
|
|
@@ -333,6 +821,7 @@ var DocxodusEditor = (() => {
|
|
|
333
821
|
measureContainer.style.visibility = "hidden";
|
|
334
822
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
335
823
|
measureContainer.style.left = "-9999px";
|
|
824
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
336
825
|
measureContainer.appendChild(child.cloneNode(true));
|
|
337
826
|
this.stagingElement.appendChild(measureContainer);
|
|
338
827
|
const childHeight = pxToPt(measureContainer.getBoundingClientRect().height);
|
|
@@ -360,6 +849,7 @@ var DocxodusEditor = (() => {
|
|
|
360
849
|
measureContainer.style.visibility = "hidden";
|
|
361
850
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
362
851
|
measureContainer.style.left = "-9999px";
|
|
852
|
+
measureContainer.className = this.cssPrefix + "footnotes";
|
|
363
853
|
measureContainer.appendChild(footnote.cloneNode(true));
|
|
364
854
|
this.stagingElement.appendChild(measureContainer);
|
|
365
855
|
const rect = measureContainer.getBoundingClientRect();
|
|
@@ -543,12 +1033,14 @@ var DocxodusEditor = (() => {
|
|
|
543
1033
|
let currentFootnoteHeight = 0;
|
|
544
1034
|
let currentContinuation = this.pendingFootnoteContinuation;
|
|
545
1035
|
let nextPageContinuation = null;
|
|
1036
|
+
let deferredFootnoteIds = [];
|
|
546
1037
|
let currentPartialFootnotes = [];
|
|
547
1038
|
if (currentContinuation && currentContinuation.remainingElements.length > 0) {
|
|
548
1039
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
549
1040
|
}
|
|
550
1041
|
const finishPage = () => {
|
|
551
|
-
|
|
1042
|
+
const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1043
|
+
if (currentContent.length === 0 && !hasCurrentContinuation) return;
|
|
552
1044
|
const page = this.createPage(
|
|
553
1045
|
dims,
|
|
554
1046
|
pageNumber,
|
|
@@ -572,15 +1064,25 @@ var DocxodusEditor = (() => {
|
|
|
572
1064
|
currentPartialFootnotes = [];
|
|
573
1065
|
currentContinuation = nextPageContinuation;
|
|
574
1066
|
nextPageContinuation = null;
|
|
1067
|
+
if (deferredFootnoteIds.length > 0) {
|
|
1068
|
+
currentFootnoteIds = [...deferredFootnoteIds];
|
|
1069
|
+
deferredFootnoteIds = [];
|
|
1070
|
+
}
|
|
575
1071
|
if (currentContinuation && currentContinuation.remainingElements.length > 0) {
|
|
576
1072
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
577
1073
|
} else {
|
|
578
1074
|
currentFootnoteHeight = 0;
|
|
579
1075
|
}
|
|
1076
|
+
if (currentFootnoteIds.length > 0) {
|
|
1077
|
+
currentFootnoteHeight += this.measureFootnotesHeight(
|
|
1078
|
+
currentFootnoteIds,
|
|
1079
|
+
dims.contentWidth,
|
|
1080
|
+
null
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
580
1083
|
};
|
|
581
1084
|
for (let i = 0; i < blocks.length; i++) {
|
|
582
1085
|
const block = blocks[i];
|
|
583
|
-
const nextBlock = blocks[i + 1];
|
|
584
1086
|
if (block.isPageBreak) {
|
|
585
1087
|
finishPage();
|
|
586
1088
|
continue;
|
|
@@ -588,8 +1090,59 @@ var DocxodusEditor = (() => {
|
|
|
588
1090
|
if (block.pageBreakBefore && currentContent.length > 0) {
|
|
589
1091
|
finishPage();
|
|
590
1092
|
}
|
|
591
|
-
const
|
|
592
|
-
const
|
|
1093
|
+
const previousBlock = blocks[i - 1];
|
|
1094
|
+
const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
|
|
1095
|
+
const pageHasOccupiedSpace = currentContent.length > 0 || currentFootnoteHeight > 0;
|
|
1096
|
+
if (pageHasOccupiedSpace && block.keepWithNext && startsKeepChain) {
|
|
1097
|
+
const keepChain = this.getKeepWithNextChain(blocks, i);
|
|
1098
|
+
if (keepChain.length > 1) {
|
|
1099
|
+
const newChainFootnoteIds = this.collectNewFootnoteIds(
|
|
1100
|
+
keepChain,
|
|
1101
|
+
currentFootnoteIds
|
|
1102
|
+
);
|
|
1103
|
+
let additionalChainFootnoteHeight = 0;
|
|
1104
|
+
if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1105
|
+
const totalChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1106
|
+
[...currentFootnoteIds, ...newChainFootnoteIds],
|
|
1107
|
+
dims.contentWidth,
|
|
1108
|
+
currentContinuation
|
|
1109
|
+
);
|
|
1110
|
+
additionalChainFootnoteHeight = Math.max(
|
|
1111
|
+
0,
|
|
1112
|
+
totalChainFootnoteHeight - currentFootnoteHeight
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1116
|
+
keepChain,
|
|
1117
|
+
prevMarginBottomPt,
|
|
1118
|
+
currentContent.length === 0
|
|
1119
|
+
) + additionalChainFootnoteHeight;
|
|
1120
|
+
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1121
|
+
if (currentChainHeight > currentAvailableHeight) {
|
|
1122
|
+
const nextPageHeights = this.getEffectiveHeights(
|
|
1123
|
+
dims,
|
|
1124
|
+
sectionIndex,
|
|
1125
|
+
pageInSection + 1,
|
|
1126
|
+
pageNumber + 1
|
|
1127
|
+
);
|
|
1128
|
+
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1129
|
+
keepChain,
|
|
1130
|
+
0,
|
|
1131
|
+
true
|
|
1132
|
+
);
|
|
1133
|
+
const freshChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1134
|
+
newChainFootnoteIds,
|
|
1135
|
+
dims.contentWidth,
|
|
1136
|
+
nextPageContinuation
|
|
1137
|
+
);
|
|
1138
|
+
if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
|
|
1139
|
+
finishPage();
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
1145
|
+
const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
|
|
593
1146
|
let additionalFootnoteHeight = 0;
|
|
594
1147
|
if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
595
1148
|
const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
|
|
@@ -606,15 +1159,23 @@ var DocxodusEditor = (() => {
|
|
|
606
1159
|
effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
|
|
607
1160
|
}
|
|
608
1161
|
const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
|
|
609
|
-
let neededHeight = blockSpace;
|
|
610
|
-
if (block.keepWithNext && nextBlock && !nextBlock.isPageBreak) {
|
|
611
|
-
const collapsedMargin = Math.max(block.marginBottomPt, nextBlock.marginTopPt);
|
|
612
|
-
neededHeight = effectiveMarginTop + block.heightPt + collapsedMargin + nextBlock.heightPt + additionalFootnoteHeight;
|
|
613
|
-
}
|
|
614
1162
|
const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
|
|
615
1163
|
const bodyContentUsed = effectiveContentHeight - remainingHeight;
|
|
616
1164
|
const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
617
1165
|
const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
|
|
1166
|
+
if (blockSpace > effectiveRemainingHeight) {
|
|
1167
|
+
const paragraphFragments = this.tryFragmentParagraph(
|
|
1168
|
+
block,
|
|
1169
|
+
dims,
|
|
1170
|
+
effectiveRemainingHeight,
|
|
1171
|
+
effectiveMarginTop
|
|
1172
|
+
);
|
|
1173
|
+
if (paragraphFragments) {
|
|
1174
|
+
blocks.splice(i, 1, ...paragraphFragments);
|
|
1175
|
+
i--;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
618
1179
|
if (blockSpace <= effectiveRemainingHeight) {
|
|
619
1180
|
currentContent.push(block.element.cloneNode(true));
|
|
620
1181
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
@@ -656,28 +1217,18 @@ var DocxodusEditor = (() => {
|
|
|
656
1217
|
footnoteId,
|
|
657
1218
|
fittingElements: fits
|
|
658
1219
|
});
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1220
|
+
if (overflow.length > 0) {
|
|
1221
|
+
nextPageContinuation = {
|
|
1222
|
+
footnoteId,
|
|
1223
|
+
remainingElements: overflow
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
663
1226
|
currentFootnoteHeight = availableForFootnotes;
|
|
664
1227
|
} else {
|
|
665
|
-
|
|
666
|
-
footnoteId,
|
|
667
|
-
remainingElements: Array.from(footnote.querySelectorAll(".footnote-content > *")).map((el) => el.cloneNode(true))
|
|
668
|
-
};
|
|
669
|
-
if (nextPageContinuation.remainingElements.length === 0) {
|
|
670
|
-
nextPageContinuation.remainingElements = [footnote.cloneNode(true)];
|
|
671
|
-
}
|
|
1228
|
+
deferredFootnoteIds.push(footnoteId);
|
|
672
1229
|
}
|
|
673
1230
|
} else {
|
|
674
|
-
|
|
675
|
-
footnoteId,
|
|
676
|
-
remainingElements: Array.from(footnote.querySelectorAll(".footnote-content > *")).map((el) => el.cloneNode(true))
|
|
677
|
-
};
|
|
678
|
-
if (nextPageContinuation.remainingElements.length === 0) {
|
|
679
|
-
nextPageContinuation.remainingElements = [footnote.cloneNode(true)];
|
|
680
|
-
}
|
|
1231
|
+
deferredFootnoteIds.push(footnoteId);
|
|
681
1232
|
}
|
|
682
1233
|
}
|
|
683
1234
|
}
|
|
@@ -692,30 +1243,39 @@ var DocxodusEditor = (() => {
|
|
|
692
1243
|
currentFootnoteHeight = expandedFootnoteSpace;
|
|
693
1244
|
} else {
|
|
694
1245
|
finishPage();
|
|
695
|
-
const newPageFootnoteHeight =
|
|
1246
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
696
1247
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
697
1248
|
currentContent.push(block.element.cloneNode(true));
|
|
698
1249
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
699
1250
|
prevMarginBottomPt = block.marginBottomPt;
|
|
700
|
-
currentFootnoteIds = [...
|
|
1251
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
701
1252
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
702
1253
|
}
|
|
703
1254
|
} else {
|
|
704
1255
|
finishPage();
|
|
705
|
-
const newPageFootnoteHeight =
|
|
1256
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
706
1257
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
707
1258
|
currentContent.push(block.element.cloneNode(true));
|
|
708
1259
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
709
1260
|
prevMarginBottomPt = block.marginBottomPt;
|
|
710
|
-
currentFootnoteIds = [...
|
|
1261
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
711
1262
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
712
1263
|
}
|
|
713
1264
|
} else {
|
|
1265
|
+
const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
|
|
1266
|
+
if (tableFragments) {
|
|
1267
|
+
if (currentContent.length > 0 || currentContinuation) {
|
|
1268
|
+
finishPage();
|
|
1269
|
+
}
|
|
1270
|
+
blocks.splice(i, 1, ...tableFragments);
|
|
1271
|
+
i--;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
714
1274
|
if (currentContent.length > 0) {
|
|
715
1275
|
finishPage();
|
|
716
1276
|
}
|
|
717
1277
|
currentContent.push(block.element.cloneNode(true));
|
|
718
|
-
currentFootnoteIds = [...
|
|
1278
|
+
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
719
1279
|
finishPage();
|
|
720
1280
|
}
|
|
721
1281
|
}
|
|
@@ -723,6 +1283,27 @@ var DocxodusEditor = (() => {
|
|
|
723
1283
|
this.pendingFootnoteContinuation = nextPageContinuation;
|
|
724
1284
|
return pages;
|
|
725
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
|
+
}
|
|
726
1307
|
/**
|
|
727
1308
|
* Creates a page container element.
|
|
728
1309
|
*/
|
|
@@ -748,6 +1329,7 @@ var DocxodusEditor = (() => {
|
|
|
748
1329
|
pageBox.style.contain = "layout paint";
|
|
749
1330
|
pageBox.dataset.pageNumber = String(pageNumber);
|
|
750
1331
|
pageBox.dataset.sectionIndex = String(sectionIndex);
|
|
1332
|
+
pageBox.dataset.pageInSection = String(pageInSection);
|
|
751
1333
|
const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
|
|
752
1334
|
const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
|
|
753
1335
|
if (headerSource) {
|
|
@@ -765,7 +1347,9 @@ var DocxodusEditor = (() => {
|
|
|
765
1347
|
headerDiv.style.justifyContent = "flex-end";
|
|
766
1348
|
headerDiv.style.paddingBottom = "4pt";
|
|
767
1349
|
for (const child of Array.from(headerSource.childNodes)) {
|
|
768
|
-
|
|
1350
|
+
const clonedheaderDiv = child.cloneNode(true);
|
|
1351
|
+
if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
|
|
1352
|
+
headerDiv.appendChild(clonedheaderDiv);
|
|
769
1353
|
}
|
|
770
1354
|
pageBox.appendChild(headerDiv);
|
|
771
1355
|
}
|
|
@@ -803,7 +1387,9 @@ var DocxodusEditor = (() => {
|
|
|
803
1387
|
footerDiv.style.justifyContent = "flex-start";
|
|
804
1388
|
footerDiv.style.paddingTop = "4pt";
|
|
805
1389
|
for (const child of Array.from(footerSource.childNodes)) {
|
|
806
|
-
|
|
1390
|
+
const clonedfooterDiv = child.cloneNode(true);
|
|
1391
|
+
if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
|
|
1392
|
+
footerDiv.appendChild(clonedfooterDiv);
|
|
807
1393
|
}
|
|
808
1394
|
pageBox.appendChild(footerDiv);
|
|
809
1395
|
}
|
|
@@ -814,6 +1400,13 @@ var DocxodusEditor = (() => {
|
|
|
814
1400
|
pageBox.appendChild(pageNum);
|
|
815
1401
|
}
|
|
816
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
|
+
}
|
|
817
1410
|
return {
|
|
818
1411
|
pageNumber,
|
|
819
1412
|
sectionIndex,
|
|
@@ -839,10 +1432,533 @@ var DocxodusEditor = (() => {
|
|
|
839
1432
|
if (!pageContainer) {
|
|
840
1433
|
throw new Error("Pagination container element not found");
|
|
841
1434
|
}
|
|
842
|
-
const engine = new PaginationEngine(staging, pageContainer,
|
|
1435
|
+
const engine = new PaginationEngine(staging, pageContainer, {
|
|
1436
|
+
...options,
|
|
1437
|
+
fragmentParagraphs: options.fragmentParagraphs ?? true
|
|
1438
|
+
});
|
|
843
1439
|
return engine.paginate();
|
|
844
1440
|
}
|
|
845
1441
|
|
|
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
|
+
];
|
|
1451
|
+
var KIND_LABELS = [
|
|
1452
|
+
{ value: "default", label: "Default" },
|
|
1453
|
+
{ value: "first", label: "First page" },
|
|
1454
|
+
{ value: "even", label: "Even pages" }
|
|
1455
|
+
];
|
|
1456
|
+
var EVEN_WARNING = "w:evenAndOddHeaders is document-global and governs footers too \u2014 even pages stop inheriting the Default stories, so a section with only a Default footer shows no footer at all on even pages.";
|
|
1457
|
+
var FIRST_WARNING = "w:titlePg makes page 1 use its own first-page header AND footer instead of the Default ones, so an empty first-page footer leaves page 1 with no footer.";
|
|
1458
|
+
var STORY_BLOCK_KINDS = /* @__PURE__ */ new Set(["p", "h", "li"]);
|
|
1459
|
+
var HeaderFooterRegion = class {
|
|
1460
|
+
constructor(bridge, handle, options, callbacks) {
|
|
1461
|
+
/** The body anchor whose section the bands currently describe (needed to seed a story). */
|
|
1462
|
+
this.bodyAnchorId = null;
|
|
1463
|
+
this.sectionInfo = null;
|
|
1464
|
+
/** Memoized body anchor → governing sectionUnid, so repeat focus costs no bridge call. */
|
|
1465
|
+
this.sectionOfAnchor = /* @__PURE__ */ new Map();
|
|
1466
|
+
this.kinds = {
|
|
1467
|
+
header: "default",
|
|
1468
|
+
footer: "default"
|
|
1469
|
+
};
|
|
1470
|
+
this.bridge = bridge;
|
|
1471
|
+
this.handle = handle;
|
|
1472
|
+
this.options = options;
|
|
1473
|
+
this.callbacks = callbacks;
|
|
1474
|
+
this.headerBand = this.buildBand("header");
|
|
1475
|
+
this.footerBand = this.buildBand("footer");
|
|
1476
|
+
}
|
|
1477
|
+
// ─── public surface ──────────────────────────────────────────────────
|
|
1478
|
+
/**
|
|
1479
|
+
* Point the bands at the section governing `bodyAnchorId` and repaint if it changed.
|
|
1480
|
+
* Cheap to call on every focus change: the anchor → section lookup is memoized, and an
|
|
1481
|
+
* unchanged `sectionUnid` returns without touching the DOM (so it never clobbers the
|
|
1482
|
+
* user's kind selection).
|
|
1483
|
+
*/
|
|
1484
|
+
syncToBody(bodyAnchorId) {
|
|
1485
|
+
if (!bodyAnchorId) return;
|
|
1486
|
+
const cached = this.sectionOfAnchor.get(bodyAnchorId);
|
|
1487
|
+
if (cached !== void 0 && cached === this.sectionInfo?.sectionUnid) {
|
|
1488
|
+
this.bodyAnchorId = bodyAnchorId;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const info = this.readSectionInfo(bodyAnchorId);
|
|
1492
|
+
if (!info) return;
|
|
1493
|
+
this.sectionOfAnchor.set(bodyAnchorId, info.sectionUnid);
|
|
1494
|
+
const changed = info.sectionUnid !== this.sectionInfo?.sectionUnid;
|
|
1495
|
+
this.bodyAnchorId = bodyAnchorId;
|
|
1496
|
+
this.sectionInfo = info;
|
|
1497
|
+
if (changed) this.refreshAll();
|
|
1498
|
+
}
|
|
1499
|
+
/** Repaint one band from the live session. */
|
|
1500
|
+
refresh(which) {
|
|
1501
|
+
this.sectionInfo = this.bodyAnchorId ? this.readSectionInfo(this.bodyAnchorId) : null;
|
|
1502
|
+
this.renderBand(which);
|
|
1503
|
+
}
|
|
1504
|
+
/** Repaint both bands from the live session (after a remount, undo/redo, or section change). */
|
|
1505
|
+
refreshAll() {
|
|
1506
|
+
this.renderBand("header");
|
|
1507
|
+
this.renderBand("footer");
|
|
1508
|
+
}
|
|
1509
|
+
/** Select (and, if absent, create) the story kind a band edits, and make it render. */
|
|
1510
|
+
setKind(which, kind) {
|
|
1511
|
+
this.kinds[which] = kind;
|
|
1512
|
+
const select = this.bandFor(which).querySelector("[data-hf-kind]");
|
|
1513
|
+
if (select && select.value !== kind) select.value = kind;
|
|
1514
|
+
if (!this.partUriFor(which, kind)) this.seedStory(which, kind);
|
|
1515
|
+
if (this.bodyAnchorId && kind !== "default") {
|
|
1516
|
+
this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind);
|
|
1517
|
+
}
|
|
1518
|
+
this.refresh(which);
|
|
1519
|
+
}
|
|
1520
|
+
/** The kind a band is currently editing. */
|
|
1521
|
+
kindOf(which) {
|
|
1522
|
+
return this.kinds[which];
|
|
1523
|
+
}
|
|
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. */
|
|
1528
|
+
insertPageNumber(which, anchorId, field) {
|
|
1529
|
+
const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field, ""));
|
|
1530
|
+
if (!res.success) return false;
|
|
1531
|
+
this.refresh(which);
|
|
1532
|
+
return true;
|
|
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
|
+
}
|
|
1581
|
+
/** Insert a page number into a band's own target paragraph (focused, else last). Seeds the
|
|
1582
|
+
* story first if the band's selected kind has none, so the command is never a silent no-op. */
|
|
1583
|
+
insertPageNumberInBand(which, field) {
|
|
1584
|
+
if (!this.partUriFor(which, this.kinds[which])) {
|
|
1585
|
+
this.seedStory(which, this.kinds[which]);
|
|
1586
|
+
this.renderBand(which);
|
|
1587
|
+
}
|
|
1588
|
+
const target = this.pageNumberTarget(which);
|
|
1589
|
+
return target ? this.insertPageNumber(which, target, field) : false;
|
|
1590
|
+
}
|
|
1591
|
+
/** The band element containing `node`, or null. */
|
|
1592
|
+
bandOf(node) {
|
|
1593
|
+
const el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
1594
|
+
const band = el?.closest("[data-hf-band]") ?? null;
|
|
1595
|
+
return band === this.headerBand || band === this.footerBand ? band : null;
|
|
1596
|
+
}
|
|
1597
|
+
/** The block-list root (a band's story container) owning `node`, or null. */
|
|
1598
|
+
blockRootOf(node) {
|
|
1599
|
+
const band = this.bandOf(node);
|
|
1600
|
+
return band ? band.querySelector("[data-hf-body]") : null;
|
|
1601
|
+
}
|
|
1602
|
+
/** True when `node` is inside either band (including its chrome). */
|
|
1603
|
+
contains(node) {
|
|
1604
|
+
return this.bandOf(node) !== null;
|
|
1605
|
+
}
|
|
1606
|
+
/** `"header"` / `"footer"` for a band element produced by this region. */
|
|
1607
|
+
whichOf(band) {
|
|
1608
|
+
return band === this.footerBand ? "footer" : "header";
|
|
1609
|
+
}
|
|
1610
|
+
// ─── section + anchor resolution ─────────────────────────────────────
|
|
1611
|
+
readSectionInfo(bodyAnchorId) {
|
|
1612
|
+
try {
|
|
1613
|
+
const raw = this.bridge.GetSectionInfo(this.handle, bodyAnchorId);
|
|
1614
|
+
const parsed = JSON.parse(raw);
|
|
1615
|
+
return parsed && typeof parsed.sectionUnid === "string" ? parsed : null;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
refsFor(which) {
|
|
1621
|
+
const info = this.sectionInfo;
|
|
1622
|
+
if (!info) return [];
|
|
1623
|
+
return (which === "header" ? info.headerRefs : info.footerRefs) ?? [];
|
|
1624
|
+
}
|
|
1625
|
+
/** The reference supplying `kind` for this band — possibly inherited from an earlier section. */
|
|
1626
|
+
refFor(which, kind) {
|
|
1627
|
+
return this.refsFor(which).find((r) => r.kind === kind) ?? null;
|
|
1628
|
+
}
|
|
1629
|
+
/** URI of the part supplying `kind` for this band, or null when the story doesn't exist. */
|
|
1630
|
+
partUriFor(which, kind) {
|
|
1631
|
+
return this.refFor(which, kind)?.partUri ?? null;
|
|
1632
|
+
}
|
|
1633
|
+
/** Full anchor ids of the story paragraphs held in `partUri`, in document order. */
|
|
1634
|
+
storyAnchors(partUri) {
|
|
1635
|
+
let index;
|
|
1636
|
+
try {
|
|
1637
|
+
index = JSON.parse(this.bridge.Project(this.handle)).anchorIndex;
|
|
1638
|
+
} catch {
|
|
1639
|
+
return [];
|
|
1640
|
+
}
|
|
1641
|
+
return Object.entries(index).filter(([, t]) => t.partUri === partUri && STORY_BLOCK_KINDS.has(t.kind)).map(([id]) => id);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Create an empty story for `kind`. Seeding happens the moment a kind is selected rather than
|
|
1645
|
+
* lazily on first keystroke: an absent story renders no contenteditable element, so there
|
|
1646
|
+
* would be nothing to click into.
|
|
1647
|
+
*/
|
|
1648
|
+
seedStory(which, kind) {
|
|
1649
|
+
if (!this.bodyAnchorId) return;
|
|
1650
|
+
const set = which === "header" ? this.bridge.SetHeaderText : this.bridge.SetFooterText;
|
|
1651
|
+
const res = parseResult(set(this.handle, this.bodyAnchorId, kind, ""));
|
|
1652
|
+
if (!res.success) return;
|
|
1653
|
+
this.callbacks.refreshAnchorMap();
|
|
1654
|
+
this.sectionInfo = this.readSectionInfo(this.bodyAnchorId);
|
|
1655
|
+
}
|
|
1656
|
+
// ─── rendering ───────────────────────────────────────────────────────
|
|
1657
|
+
bandFor(which) {
|
|
1658
|
+
return which === "header" ? this.headerBand : this.footerBand;
|
|
1659
|
+
}
|
|
1660
|
+
buildBand(which) {
|
|
1661
|
+
const band = document.createElement("div");
|
|
1662
|
+
band.className = "docx-hf-band";
|
|
1663
|
+
band.setAttribute("data-hf-band", which);
|
|
1664
|
+
const chrome = document.createElement("div");
|
|
1665
|
+
chrome.className = "docx-hf-chrome";
|
|
1666
|
+
const label = document.createElement("span");
|
|
1667
|
+
label.className = "docx-hf-label";
|
|
1668
|
+
label.textContent = which === "header" ? "Header" : "Footer";
|
|
1669
|
+
chrome.appendChild(label);
|
|
1670
|
+
const kindSelect = document.createElement("select");
|
|
1671
|
+
kindSelect.setAttribute("data-hf-kind", "");
|
|
1672
|
+
kindSelect.title = "Which header/footer story to edit";
|
|
1673
|
+
for (const k of KIND_LABELS) {
|
|
1674
|
+
const opt = document.createElement("option");
|
|
1675
|
+
opt.value = k.value;
|
|
1676
|
+
opt.textContent = k.label;
|
|
1677
|
+
kindSelect.appendChild(opt);
|
|
1678
|
+
}
|
|
1679
|
+
kindSelect.addEventListener(
|
|
1680
|
+
"change",
|
|
1681
|
+
() => this.setKind(which, kindSelect.value)
|
|
1682
|
+
);
|
|
1683
|
+
chrome.appendChild(kindSelect);
|
|
1684
|
+
const pageNum = document.createElement("select");
|
|
1685
|
+
pageNum.setAttribute("data-hf-pagenum", "");
|
|
1686
|
+
pageNum.title = "Insert a page-number field into the focused story paragraph";
|
|
1687
|
+
for (const o of [
|
|
1688
|
+
{ value: "", label: "Page \u2116\u2026" },
|
|
1689
|
+
{ value: "currentPage", label: "Page number" },
|
|
1690
|
+
{ value: "totalPages", label: "Total pages" }
|
|
1691
|
+
]) {
|
|
1692
|
+
const opt = document.createElement("option");
|
|
1693
|
+
opt.value = o.value;
|
|
1694
|
+
opt.textContent = o.label;
|
|
1695
|
+
pageNum.appendChild(opt);
|
|
1696
|
+
}
|
|
1697
|
+
pageNum.addEventListener("change", () => {
|
|
1698
|
+
const field = pageNum.value;
|
|
1699
|
+
pageNum.value = "";
|
|
1700
|
+
if (field !== "currentPage" && field !== "totalPages") return;
|
|
1701
|
+
const target = this.pageNumberTarget(which);
|
|
1702
|
+
if (target) this.insertPageNumber(which, target, field);
|
|
1703
|
+
});
|
|
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);
|
|
1740
|
+
const inheritedNote = document.createElement("span");
|
|
1741
|
+
inheritedNote.className = "docx-hf-inherited";
|
|
1742
|
+
inheritedNote.setAttribute("data-hf-inherited-note", "");
|
|
1743
|
+
inheritedNote.hidden = true;
|
|
1744
|
+
chrome.appendChild(inheritedNote);
|
|
1745
|
+
band.appendChild(chrome);
|
|
1746
|
+
const warning = document.createElement("div");
|
|
1747
|
+
warning.className = "docx-hf-warning";
|
|
1748
|
+
warning.setAttribute("data-hf-warning", "");
|
|
1749
|
+
warning.hidden = true;
|
|
1750
|
+
band.appendChild(warning);
|
|
1751
|
+
const body = document.createElement("div");
|
|
1752
|
+
body.className = "docx-hf-body";
|
|
1753
|
+
body.setAttribute("data-hf-body", "");
|
|
1754
|
+
band.appendChild(body);
|
|
1755
|
+
return band;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Which story paragraph a page-number insert targets: the last one the user focused in this
|
|
1759
|
+
* band if it is still attached, else the band's last paragraph (Word's convention — the page
|
|
1760
|
+
* number goes at the end of the footer line).
|
|
1761
|
+
*/
|
|
1762
|
+
pageNumberTarget(which) {
|
|
1763
|
+
const body = this.bandFor(which).querySelector("[data-hf-body]");
|
|
1764
|
+
if (!body) return null;
|
|
1765
|
+
const focused = body.querySelector('[data-hf-focused="true"][data-hf-anchor]');
|
|
1766
|
+
const blocks = Array.from(body.querySelectorAll("[data-hf-anchor]"));
|
|
1767
|
+
const target = focused ?? blocks[blocks.length - 1];
|
|
1768
|
+
return target?.getAttribute("data-hf-anchor") ?? null;
|
|
1769
|
+
}
|
|
1770
|
+
renderBand(which) {
|
|
1771
|
+
const band = this.bandFor(which);
|
|
1772
|
+
const body = band.querySelector("[data-hf-body]");
|
|
1773
|
+
if (!body) return;
|
|
1774
|
+
const kind = this.kinds[which];
|
|
1775
|
+
const select = band.querySelector("[data-hf-kind]");
|
|
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
|
+
}
|
|
1784
|
+
const inherited = this.refFor(which, kind)?.inherited === true;
|
|
1785
|
+
band.toggleAttribute("data-hf-inherited", inherited);
|
|
1786
|
+
const note = band.querySelector("[data-hf-inherited-note]");
|
|
1787
|
+
if (note) {
|
|
1788
|
+
note.hidden = !inherited;
|
|
1789
|
+
note.textContent = inherited ? "inherited from an earlier section" : "";
|
|
1790
|
+
}
|
|
1791
|
+
this.renderKindWarning(which);
|
|
1792
|
+
body.innerHTML = "";
|
|
1793
|
+
const partUri = this.partUriFor(which, kind);
|
|
1794
|
+
if (!partUri) {
|
|
1795
|
+
body.appendChild(this.placeholder(which, kind));
|
|
1796
|
+
band.setAttribute("data-hf-empty", "true");
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
band.removeAttribute("data-hf-empty");
|
|
1800
|
+
const anchors = this.storyAnchors(partUri);
|
|
1801
|
+
if (anchors.length === 0) {
|
|
1802
|
+
body.appendChild(this.placeholder(which, kind));
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
for (const anchorId of anchors) {
|
|
1806
|
+
const el = this.renderStoryBlock(anchorId);
|
|
1807
|
+
if (!el) continue;
|
|
1808
|
+
body.appendChild(el);
|
|
1809
|
+
this.adoptBlock(el, anchorId);
|
|
1810
|
+
this.callbacks.wireBlock(el);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Mark a story paragraph as belonging to this region. The full anchor id is stamped alongside
|
|
1815
|
+
* `data-anchor` (which carries only the bare unid, matching the body's convention) so band
|
|
1816
|
+
* chrome can address the block without going through the editor's unid map. Called on first
|
|
1817
|
+
* render AND after an incremental swap, which replaces the DOM node.
|
|
1818
|
+
*/
|
|
1819
|
+
adoptBlock(el, anchorId) {
|
|
1820
|
+
const body = this.blockRootOf(el);
|
|
1821
|
+
if (!body) return;
|
|
1822
|
+
el.setAttribute("data-hf-anchor", anchorId);
|
|
1823
|
+
if (el.dataset.hfAdopted === "true") return;
|
|
1824
|
+
el.dataset.hfAdopted = "true";
|
|
1825
|
+
el.addEventListener("focus", () => {
|
|
1826
|
+
body.querySelectorAll("[data-hf-focused]").forEach((sib) => sib.removeAttribute("data-hf-focused"));
|
|
1827
|
+
el.setAttribute("data-hf-focused", "true");
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
renderStoryBlock(anchorId) {
|
|
1831
|
+
const html = this.bridge.RenderBlockHtml(
|
|
1832
|
+
this.handle,
|
|
1833
|
+
anchorId,
|
|
1834
|
+
this.options.cssPrefix,
|
|
1835
|
+
this.options.fabricateClasses
|
|
1836
|
+
);
|
|
1837
|
+
if (html.charCodeAt(0) === 123) return null;
|
|
1838
|
+
return new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
1839
|
+
}
|
|
1840
|
+
placeholder(which, kind) {
|
|
1841
|
+
const el = document.createElement("div");
|
|
1842
|
+
el.className = "docx-hf-placeholder";
|
|
1843
|
+
el.setAttribute("data-hf-placeholder", "");
|
|
1844
|
+
const label = KIND_LABELS.find((k) => k.value === kind)?.label ?? kind;
|
|
1845
|
+
el.textContent = `No ${label.toLowerCase()} ${which} \u2014 pick a kind to create one.`;
|
|
1846
|
+
return el;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Surface the caveat that comes with the selected kind. Turning on first/even means those pages
|
|
1850
|
+
* stop using the Default stories entirely, which bites hardest on the OTHER band: enabling an
|
|
1851
|
+
* even header with no even footer leaves even pages footer-less, and enabling a first-page
|
|
1852
|
+
* header with an empty first-page footer leaves page 1 footer-less. The note is shown whenever
|
|
1853
|
+
* first/even is selected (the behavior change is real either way); the fix button appears only
|
|
1854
|
+
* when the counterpart story is missing entirely, which is what a user almost always wants next.
|
|
1855
|
+
*/
|
|
1856
|
+
renderKindWarning(which) {
|
|
1857
|
+
const band = this.bandFor(which);
|
|
1858
|
+
const warning = band.querySelector("[data-hf-warning]");
|
|
1859
|
+
if (!warning) return;
|
|
1860
|
+
const kind = this.kinds[which];
|
|
1861
|
+
if (kind === "default") {
|
|
1862
|
+
warning.hidden = true;
|
|
1863
|
+
warning.textContent = "";
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const other = which === "header" ? "footer" : "header";
|
|
1867
|
+
warning.hidden = false;
|
|
1868
|
+
warning.textContent = `${kind === "even" ? EVEN_WARNING : FIRST_WARNING} `;
|
|
1869
|
+
if (this.partUriFor(other, kind)) return;
|
|
1870
|
+
const fix = document.createElement("button");
|
|
1871
|
+
fix.type = "button";
|
|
1872
|
+
fix.setAttribute("data-hf-fix-counterpart", "");
|
|
1873
|
+
fix.textContent = `Also create a ${kind === "even" ? "matching even" : "first-page"} ${other}`;
|
|
1874
|
+
fix.addEventListener("click", () => {
|
|
1875
|
+
this.setKind(other, kind);
|
|
1876
|
+
this.renderKindWarning(which);
|
|
1877
|
+
});
|
|
1878
|
+
warning.appendChild(fix);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
function parseResult(json) {
|
|
1882
|
+
try {
|
|
1883
|
+
return JSON.parse(json);
|
|
1884
|
+
} catch {
|
|
1885
|
+
return { success: false };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
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
|
+
|
|
846
1962
|
// src/editor.ts
|
|
847
1963
|
var EDITABLE_TAGS = /* @__PURE__ */ new Set(["P", "H1", "H2", "H3", "H4", "H5", "H6"]);
|
|
848
1964
|
function fontWeightIsBold(w) {
|
|
@@ -855,7 +1971,7 @@ var DocxodusEditor = (() => {
|
|
|
855
1971
|
}
|
|
856
1972
|
function collectInlineSegments(node, out) {
|
|
857
1973
|
node.childNodes.forEach((child) => {
|
|
858
|
-
if (child
|
|
1974
|
+
if (isGeneratedChrome(child)) return;
|
|
859
1975
|
if (child.nodeType === 3) {
|
|
860
1976
|
const text = child.textContent ?? "";
|
|
861
1977
|
if (!text) return;
|
|
@@ -909,10 +2025,14 @@ var DocxodusEditor = (() => {
|
|
|
909
2025
|
function isListBlock(block) {
|
|
910
2026
|
return !!block.querySelector(":scope > [data-list-marker]");
|
|
911
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
|
+
}
|
|
912
2032
|
function isInMarker(node) {
|
|
913
2033
|
let el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
914
2034
|
while (el) {
|
|
915
|
-
if (el
|
|
2035
|
+
if (isGeneratedChrome(el)) return true;
|
|
916
2036
|
el = el.parentElement;
|
|
917
2037
|
}
|
|
918
2038
|
return false;
|
|
@@ -976,11 +2096,22 @@ var DocxodusEditor = (() => {
|
|
|
976
2096
|
return out;
|
|
977
2097
|
}
|
|
978
2098
|
function trimmedSplitOffset(block, domOffset) {
|
|
979
|
-
const
|
|
980
|
-
const leading = content.length - content.replace(/^\s+/, "").length;
|
|
981
|
-
const trimmedLen = content.trim().length;
|
|
2099
|
+
const { leading, trimmedLen } = trimBounds(block);
|
|
982
2100
|
return Math.max(0, Math.min(domOffset - Math.min(domOffset, leading), trimmedLen));
|
|
983
2101
|
}
|
|
2102
|
+
function trimBounds(block) {
|
|
2103
|
+
const content = blockContentText(block);
|
|
2104
|
+
return {
|
|
2105
|
+
leading: content.length - content.replace(/^\s+/, "").length,
|
|
2106
|
+
trimmedLen: content.trim().length
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function trimmedSpan(block, span) {
|
|
2110
|
+
const { leading, trimmedLen } = trimBounds(block);
|
|
2111
|
+
const start = Math.max(0, Math.min(span.start - Math.min(span.start, leading), trimmedLen));
|
|
2112
|
+
const end = Math.max(start, Math.min(span.start + span.length - leading, trimmedLen));
|
|
2113
|
+
return { start, length: end - start };
|
|
2114
|
+
}
|
|
984
2115
|
function contentPositionIn(el, offset) {
|
|
985
2116
|
let remaining = offset;
|
|
986
2117
|
let result = null;
|
|
@@ -1047,7 +2178,8 @@ var DocxodusEditor = (() => {
|
|
|
1047
2178
|
if (!block.contains(range.startContainer) || !block.contains(range.endContainer)) return null;
|
|
1048
2179
|
const start = contentOffsetOf(block, range.startContainer, range.startOffset);
|
|
1049
2180
|
const end = contentOffsetOf(block, range.endContainer, range.endOffset);
|
|
1050
|
-
|
|
2181
|
+
const span = trimmedSpan(block, { start: Math.min(start, end), length: Math.abs(end - start) });
|
|
2182
|
+
return span.length > 0 ? span : null;
|
|
1051
2183
|
}
|
|
1052
2184
|
function selectRange(el, start, length) {
|
|
1053
2185
|
const sel = typeof window !== "undefined" ? window.getSelection() : null;
|
|
@@ -1132,8 +2264,11 @@ var DocxodusEditor = (() => {
|
|
|
1132
2264
|
false,
|
|
1133
2265
|
0,
|
|
1134
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.
|
|
1135
2270
|
/* renderFootnotesAndEndnotes */
|
|
1136
|
-
|
|
2271
|
+
true,
|
|
1137
2272
|
/* renderHeadersAndFooters */
|
|
1138
2273
|
paginated,
|
|
1139
2274
|
false,
|
|
@@ -1167,6 +2302,11 @@ var DocxodusEditor = (() => {
|
|
|
1167
2302
|
* block, and cleared when a caret is collapsed inside a block (so it never goes stale).
|
|
1168
2303
|
*/
|
|
1169
2304
|
this.lastSelection = null;
|
|
2305
|
+
/** The docked header/footer bands, when `options.headerFooter` is on. */
|
|
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;
|
|
1170
2310
|
/** Track the last meaningful selection so focus-stealing toolbar controls can still target it. */
|
|
1171
2311
|
this.onSelectionChange = () => {
|
|
1172
2312
|
if (this.closed) return;
|
|
@@ -1192,12 +2332,43 @@ var DocxodusEditor = (() => {
|
|
|
1192
2332
|
if (typeof document !== "undefined")
|
|
1193
2333
|
document.addEventListener("selectionchange", this.onSelectionChange);
|
|
1194
2334
|
}
|
|
1195
|
-
/** The editable block (contenteditable [data-anchor]) containing `node`, if any, within this editor.
|
|
2335
|
+
/** The editable block (contenteditable [data-anchor]) containing `node`, if any, within this editor.
|
|
2336
|
+
* Fenced by `container`, not `editRoot`, so header/footer band blocks — which live outside the
|
|
2337
|
+
* body edit root by design — also register. The fence still rejects other editors on the page. */
|
|
1196
2338
|
editableBlockOf(node) {
|
|
1197
2339
|
if (!node) return null;
|
|
1198
2340
|
const start = node.nodeType === 1 ? node : node.parentElement;
|
|
1199
2341
|
const block = start?.closest('[data-anchor][contenteditable="true"]') ?? null;
|
|
1200
|
-
return block && this.
|
|
2342
|
+
return block && this.container.contains(block) ? block : null;
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* The root owning `el`'s sibling block list: its header/footer band's story container, else the
|
|
2346
|
+
* body edit root. Keeps a multi-block selection from spanning a band and the body, whose block
|
|
2347
|
+
* lists belong to different OOXML parts.
|
|
2348
|
+
*/
|
|
2349
|
+
ownerRoot(el) {
|
|
2350
|
+
return this.region?.blockRootOf(el) ?? this.editRoot;
|
|
2351
|
+
}
|
|
2352
|
+
/** True when `el` is a header/footer band block rather than a body block. */
|
|
2353
|
+
isBandBlock(el) {
|
|
2354
|
+
return !!this.region?.contains(el);
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Repaint after an edit to `block` that would otherwise remount the whole document: a band
|
|
2358
|
+
* repaints only itself (a story is one to three paragraphs), leaving the body DOM — and the
|
|
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).
|
|
2363
|
+
*/
|
|
2364
|
+
refreshAfter(block, focusIndex, caretAtEnd = false, forceRemount = false) {
|
|
2365
|
+
const band = this.region?.bandOf(block);
|
|
2366
|
+
if (band) {
|
|
2367
|
+
this.region.refresh(this.region.whichOf(band));
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
if (forceRemount) this.remount(focusIndex, caretAtEnd);
|
|
2371
|
+
else this.reconcile(focusIndex, caretAtEnd);
|
|
1201
2372
|
}
|
|
1202
2373
|
/** Open a document, render it into `container`, and wire up editing. */
|
|
1203
2374
|
static open(container, bytes, exports, options = {}) {
|
|
@@ -1207,16 +2378,19 @@ var DocxodusEditor = (() => {
|
|
|
1207
2378
|
editable: options.editable ?? true,
|
|
1208
2379
|
paginated: options.paginated ?? false,
|
|
1209
2380
|
scale: options.scale ?? 1,
|
|
2381
|
+
headerFooter: options.headerFooter ?? false,
|
|
1210
2382
|
onEdit: options.onEdit
|
|
1211
2383
|
};
|
|
1212
|
-
const handle = exports.DocxSessionBridge.OpenSession(bytes, '{"
|
|
2384
|
+
const handle = exports.DocxSessionBridge.OpenSession(bytes, '{"emitMarkdownPatch":false}');
|
|
1213
2385
|
const editor = new _DocxEditor(container, exports, handle, opts);
|
|
1214
2386
|
editor.refreshAnchorMap();
|
|
2387
|
+
if (opts.headerFooter) editor.createRegion();
|
|
1215
2388
|
const fullHtml = exports.DocumentConverter.ConvertDocxToHtmlComplete(
|
|
1216
2389
|
...completeArgs(bytes, opts.cssPrefix, opts.fabricateClasses, opts.paginated, opts.scale)
|
|
1217
2390
|
);
|
|
1218
2391
|
if (opts.paginated) editor.mountPaginated(fullHtml);
|
|
1219
2392
|
else editor.mountHtml(fullHtml);
|
|
2393
|
+
editor.syncRegionToBody();
|
|
1220
2394
|
return editor;
|
|
1221
2395
|
}
|
|
1222
2396
|
/**
|
|
@@ -1259,29 +2433,117 @@ var DocxodusEditor = (() => {
|
|
|
1259
2433
|
assertOpen() {
|
|
1260
2434
|
if (this.closed) throw new Error("DocxEditor is closed");
|
|
1261
2435
|
}
|
|
1262
|
-
/**
|
|
2436
|
+
/**
|
|
2437
|
+
* Rebuild unid → full-anchor-id from the live session projection.
|
|
2438
|
+
*
|
|
2439
|
+
* Unids are CONTENT-ADDRESSED, so blocks with identical content in DIFFERENT parts collide —
|
|
2440
|
+
* e.g. a document with empty default/first/even header stories has one unid for several
|
|
2441
|
+
* header parts. A collision must resolve to the BODY entry: body blocks carry only
|
|
2442
|
+
* `data-anchor` (the bare unid) and have nothing else to resolve through, whereas a
|
|
2443
|
+
* header/footer band block carries its full anchor in `data-hf-anchor` and is resolved from
|
|
2444
|
+
* that (see `anchorIdOf`). Letting a non-body scope win here would silently redirect a body
|
|
2445
|
+
* edit into a header part.
|
|
2446
|
+
*/
|
|
1263
2447
|
refreshAnchorMap() {
|
|
1264
|
-
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);
|
|
1265
2451
|
this.unidToFullId.clear();
|
|
2452
|
+
const bodyOwned = /* @__PURE__ */ new Set();
|
|
1266
2453
|
for (const [fullId, target] of Object.entries(proj.anchorIndex)) {
|
|
2454
|
+
const isBody = target.scope === "body";
|
|
2455
|
+
if (!isBody && bodyOwned.has(target.unid)) continue;
|
|
1267
2456
|
this.unidToFullId.set(target.unid, fullId);
|
|
2457
|
+
if (isBody) bodyOwned.add(target.unid);
|
|
1268
2458
|
}
|
|
1269
2459
|
}
|
|
2460
|
+
/**
|
|
2461
|
+
* The full `kind:scope:unid` anchor for a rendered block. A header/footer band block carries
|
|
2462
|
+
* its own — the unid map cannot disambiguate one, since several parts' story paragraphs can
|
|
2463
|
+
* share a content-addressed unid (a real Word document with empty default/first/even stories
|
|
2464
|
+
* does exactly that, and a unid-keyed lookup would land the edit in the wrong header part).
|
|
2465
|
+
*/
|
|
2466
|
+
anchorIdOf(el) {
|
|
2467
|
+
const stamped = el.getAttribute("data-hf-anchor");
|
|
2468
|
+
if (stamped) return stamped;
|
|
2469
|
+
const unid = el.getAttribute("data-anchor");
|
|
2470
|
+
return unid ? this.unidToFullId.get(unid) : void 0;
|
|
2471
|
+
}
|
|
2472
|
+
/** Build the header/footer region (called once, before the first mount). */
|
|
2473
|
+
createRegion() {
|
|
2474
|
+
this.region = new HeaderFooterRegion(
|
|
2475
|
+
this.exports.DocxSessionBridge,
|
|
2476
|
+
this.handle,
|
|
2477
|
+
{ cssPrefix: this.options.cssPrefix, fabricateClasses: this.options.fabricateClasses },
|
|
2478
|
+
{
|
|
2479
|
+
wireBlock: (el) => this.wireBlock(el),
|
|
2480
|
+
refreshAnchorMap: () => this.refreshAnchorMap()
|
|
2481
|
+
}
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Point the bands at the section governing the current focus (or the first body block).
|
|
2486
|
+
* Called after every mount, and on focus of a body block, so a multi-section document shows
|
|
2487
|
+
* the stories that actually apply where the caret is.
|
|
2488
|
+
*/
|
|
2489
|
+
syncRegionToBody(fromBlock) {
|
|
2490
|
+
if (!this.region) return;
|
|
2491
|
+
const block = fromBlock ?? this.editableList()[0];
|
|
2492
|
+
const unid = block?.getAttribute("data-anchor");
|
|
2493
|
+
const id = unid ? this.unidToFullId.get(unid) : void 0;
|
|
2494
|
+
this.region.syncToBody(id ?? null);
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Insert the bands around `bodyRoot` and make `bodyRoot` the edit root. Band blocks must stay
|
|
2498
|
+
* OUT of the edit root: `editableList()`/`blockIndex()` enumerate it to compute remount focus
|
|
2499
|
+
* indices, and band blocks in that list would shift every index.
|
|
2500
|
+
*/
|
|
2501
|
+
dockBands(bodyRoot) {
|
|
2502
|
+
if (!this.region) return;
|
|
2503
|
+
bodyRoot.before(this.region.headerBand);
|
|
2504
|
+
bodyRoot.after(this.region.footerBand);
|
|
2505
|
+
this.region.refreshAll();
|
|
2506
|
+
}
|
|
1270
2507
|
/** Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks. */
|
|
1271
2508
|
mountHtml(fullHtml) {
|
|
1272
2509
|
const parsed = new DOMParser().parseFromString(fullHtml, "text/html");
|
|
1273
2510
|
const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
|
|
1274
|
-
this.
|
|
1275
|
-
|
|
1276
|
-
|
|
2511
|
+
if (!this.region) {
|
|
2512
|
+
this.container.innerHTML = styles + parsed.body.innerHTML;
|
|
2513
|
+
this.editRoot = this.container;
|
|
2514
|
+
if (this.options.editable) this.wireBlocks(this.container);
|
|
2515
|
+
this.stampPlanState();
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
this.container.innerHTML = styles;
|
|
2519
|
+
const flow = document.createElement("div");
|
|
2520
|
+
flow.className = "docx-body-flow";
|
|
2521
|
+
flow.innerHTML = parsed.body.innerHTML;
|
|
2522
|
+
this.container.appendChild(flow);
|
|
2523
|
+
this.editRoot = flow;
|
|
2524
|
+
if (this.options.editable) this.wireBlocks(flow);
|
|
2525
|
+
this.stampPlanState();
|
|
2526
|
+
this.dockBands(flow);
|
|
1277
2527
|
}
|
|
1278
2528
|
/** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
|
|
1279
2529
|
mountPaginated(fullHtml) {
|
|
1280
|
-
|
|
2530
|
+
let target = this.container;
|
|
2531
|
+
if (this.region) {
|
|
2532
|
+
this.container.innerHTML = "";
|
|
2533
|
+
target = document.createElement("div");
|
|
2534
|
+
target.className = "docx-body-flow";
|
|
2535
|
+
this.container.appendChild(target);
|
|
2536
|
+
}
|
|
2537
|
+
paginateHtml(fullHtml, target, {
|
|
2538
|
+
scale: this.options.scale,
|
|
2539
|
+
cssPrefix: "page-",
|
|
2540
|
+
fragmentParagraphs: false
|
|
2541
|
+
});
|
|
1281
2542
|
this.container.querySelector("#pagination-staging, .page-staging")?.remove();
|
|
1282
|
-
const pageRoot =
|
|
2543
|
+
const pageRoot = target.querySelector("#pagination-container") ?? target;
|
|
1283
2544
|
this.editRoot = pageRoot;
|
|
1284
2545
|
if (this.options.editable) this.wireBlocks(pageRoot);
|
|
2546
|
+
if (this.region) this.dockBands(target);
|
|
1285
2547
|
}
|
|
1286
2548
|
wireBlocks(root) {
|
|
1287
2549
|
root.querySelectorAll("[data-anchor]").forEach((el) => this.wireBlock(el));
|
|
@@ -1289,15 +2551,18 @@ var DocxodusEditor = (() => {
|
|
|
1289
2551
|
wireBlock(el) {
|
|
1290
2552
|
if (!EDITABLE_TAGS.has(el.tagName)) return;
|
|
1291
2553
|
const unid = el.getAttribute("data-anchor");
|
|
1292
|
-
if (!unid || !this.
|
|
2554
|
+
if (!unid || !this.anchorIdOf(el)) return;
|
|
1293
2555
|
el.setAttribute("contenteditable", "true");
|
|
1294
|
-
el.querySelectorAll(
|
|
2556
|
+
el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
|
|
1295
2557
|
el.dataset.committedText = blockContentText(el);
|
|
1296
2558
|
el.addEventListener("focus", () => {
|
|
1297
2559
|
this.activeBlock = el;
|
|
2560
|
+
if (this.region && !this.isBandBlock(el)) this.syncRegionToBody(el);
|
|
1298
2561
|
});
|
|
1299
2562
|
el.addEventListener("blur", () => this.commitBlock(el));
|
|
1300
2563
|
el.addEventListener("keydown", (ev) => this.onKeydown(el, ev));
|
|
2564
|
+
const stamped = el.getAttribute("data-hf-anchor");
|
|
2565
|
+
if (stamped && this.region?.contains(el)) this.region.adoptBlock(el, stamped);
|
|
1301
2566
|
}
|
|
1302
2567
|
/**
|
|
1303
2568
|
* Replace `oldEl` with `newNodes`, suppressing the re-entrant blur→commit that removing a focused
|
|
@@ -1327,7 +2592,7 @@ var DocxodusEditor = (() => {
|
|
|
1327
2592
|
if (this.closed || this.replacing) return;
|
|
1328
2593
|
const unid = el.getAttribute("data-anchor");
|
|
1329
2594
|
if (!unid) return;
|
|
1330
|
-
const fullId = this.
|
|
2595
|
+
const fullId = this.anchorIdOf(el);
|
|
1331
2596
|
if (!fullId) return;
|
|
1332
2597
|
const result = this.commitTextChange(el, fullId);
|
|
1333
2598
|
if (!result) return;
|
|
@@ -1354,11 +2619,17 @@ var DocxodusEditor = (() => {
|
|
|
1354
2619
|
);
|
|
1355
2620
|
if (html.charCodeAt(0) !== 123) {
|
|
1356
2621
|
const fresh = new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
2622
|
+
const inBand = this.isBandBlock(el);
|
|
1357
2623
|
if (fresh && this.replaceNode(el, fresh)) {
|
|
1358
|
-
|
|
1359
|
-
|
|
2624
|
+
if (inBand) {
|
|
2625
|
+
this.region.adoptBlock(fresh, newAnchor);
|
|
2626
|
+
} else {
|
|
2627
|
+
this.unidToFullId.delete(unid);
|
|
2628
|
+
this.unidToFullId.set(newUnid, newAnchor);
|
|
2629
|
+
}
|
|
1360
2630
|
this.wireBlock(fresh);
|
|
1361
2631
|
if (this.activeBlock === el) this.activeBlock = fresh;
|
|
2632
|
+
this.maybeRenumberNotes(fresh);
|
|
1362
2633
|
}
|
|
1363
2634
|
}
|
|
1364
2635
|
this.options.onEdit?.({ anchorId: newAnchor, unid: newUnid });
|
|
@@ -1432,7 +2703,7 @@ var DocxodusEditor = (() => {
|
|
|
1432
2703
|
const rawOffset = caretOffsetIn(el);
|
|
1433
2704
|
const unid = el.getAttribute("data-anchor");
|
|
1434
2705
|
if (rawOffset == null || !unid) return;
|
|
1435
|
-
let fullId = this.
|
|
2706
|
+
let fullId = this.anchorIdOf(el);
|
|
1436
2707
|
if (!fullId) return;
|
|
1437
2708
|
const offset = trimmedSplitOffset(el, rawOffset);
|
|
1438
2709
|
const idx = this.blockIndex(el);
|
|
@@ -1444,17 +2715,23 @@ var DocxodusEditor = (() => {
|
|
|
1444
2715
|
const second = res.created?.[0];
|
|
1445
2716
|
if (!first || !second) return;
|
|
1446
2717
|
if (this.affectsList(res) || wrappedInBorder) {
|
|
1447
|
-
this.
|
|
2718
|
+
this.refreshAfter(el, idx + 1, false);
|
|
1448
2719
|
this.options.onEdit?.({ anchorId: second.id, unid: second.unid });
|
|
1449
2720
|
return;
|
|
1450
2721
|
}
|
|
1451
2722
|
const firstEl = this.renderInto(first.id);
|
|
1452
2723
|
const secondEl = this.renderInto(second.id);
|
|
1453
2724
|
if (!firstEl || !secondEl) return;
|
|
2725
|
+
const inBand = this.isBandBlock(el);
|
|
1454
2726
|
if (!this.replaceNode(el, firstEl, secondEl)) return;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2727
|
+
if (inBand) {
|
|
2728
|
+
this.region.adoptBlock(firstEl, first.id);
|
|
2729
|
+
this.region.adoptBlock(secondEl, second.id);
|
|
2730
|
+
} else {
|
|
2731
|
+
this.unidToFullId.delete(unid);
|
|
2732
|
+
this.unidToFullId.set(first.unid, first.id);
|
|
2733
|
+
this.unidToFullId.set(second.unid, second.id);
|
|
2734
|
+
}
|
|
1458
2735
|
this.wireBlock(firstEl);
|
|
1459
2736
|
this.wireBlock(secondEl);
|
|
1460
2737
|
placeCaretAtOffset(secondEl, 0);
|
|
@@ -1465,8 +2742,8 @@ var DocxodusEditor = (() => {
|
|
|
1465
2742
|
const prevUnid = prev.getAttribute("data-anchor");
|
|
1466
2743
|
const thisUnid = el.getAttribute("data-anchor");
|
|
1467
2744
|
if (!prevUnid || !thisUnid) return;
|
|
1468
|
-
let prevId = this.
|
|
1469
|
-
let thisId = this.
|
|
2745
|
+
let prevId = this.anchorIdOf(prev);
|
|
2746
|
+
let thisId = this.anchorIdOf(el);
|
|
1470
2747
|
if (!prevId || !thisId) return;
|
|
1471
2748
|
const prevIdx = this.blockIndex(prev);
|
|
1472
2749
|
const wrappedInBorder = inBorderWrapper(prev) || inBorderWrapper(el);
|
|
@@ -1478,17 +2755,22 @@ var DocxodusEditor = (() => {
|
|
|
1478
2755
|
const merged = res.modified?.[0];
|
|
1479
2756
|
if (!merged) return;
|
|
1480
2757
|
if (this.affectsList(res) || wrappedInBorder) {
|
|
1481
|
-
this.
|
|
2758
|
+
this.refreshAfter(el, prevIdx, true);
|
|
1482
2759
|
this.options.onEdit?.({ anchorId: merged.id, unid: merged.unid });
|
|
1483
2760
|
return;
|
|
1484
2761
|
}
|
|
1485
2762
|
const mergedEl = this.renderInto(merged.id);
|
|
1486
2763
|
if (!mergedEl) return;
|
|
2764
|
+
const inBand = this.isBandBlock(prev);
|
|
1487
2765
|
if (!this.replaceNode(prev, mergedEl)) return;
|
|
1488
2766
|
el.remove();
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2767
|
+
if (inBand) {
|
|
2768
|
+
this.region.adoptBlock(mergedEl, merged.id);
|
|
2769
|
+
} else {
|
|
2770
|
+
this.unidToFullId.delete(prevUnid);
|
|
2771
|
+
this.unidToFullId.delete(thisUnid);
|
|
2772
|
+
this.unidToFullId.set(merged.unid, merged.id);
|
|
2773
|
+
}
|
|
1492
2774
|
this.wireBlock(mergedEl);
|
|
1493
2775
|
placeCaretAtOffset(mergedEl, caret);
|
|
1494
2776
|
this.options.onEdit?.({ anchorId: merged.id, unid: merged.unid });
|
|
@@ -1551,10 +2833,10 @@ var DocxodusEditor = (() => {
|
|
|
1551
2833
|
if (html.charCodeAt(0) === 123) return null;
|
|
1552
2834
|
return new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
1553
2835
|
}
|
|
1554
|
-
/** The editable block immediately before `el`
|
|
2836
|
+
/** The editable block immediately before `el` within its own root, or null. */
|
|
1555
2837
|
previousEditable(el) {
|
|
1556
2838
|
const all = Array.from(
|
|
1557
|
-
this.
|
|
2839
|
+
this.ownerRoot(el).querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
1558
2840
|
);
|
|
1559
2841
|
const i = all.indexOf(el);
|
|
1560
2842
|
return i > 0 ? all[i - 1] : null;
|
|
@@ -1574,8 +2856,9 @@ var DocxodusEditor = (() => {
|
|
|
1574
2856
|
* A collapsed or single-block selection yields just the active block. */
|
|
1575
2857
|
selectedBlocks() {
|
|
1576
2858
|
const sel = typeof window !== "undefined" ? window.getSelection() : null;
|
|
2859
|
+
const root = this.activeBlock ? this.ownerRoot(this.activeBlock) : this.editRoot;
|
|
1577
2860
|
const all = Array.from(
|
|
1578
|
-
|
|
2861
|
+
root.querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
1579
2862
|
);
|
|
1580
2863
|
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
|
|
1581
2864
|
const range = sel.getRangeAt(0);
|
|
@@ -1605,13 +2888,13 @@ var DocxodusEditor = (() => {
|
|
|
1605
2888
|
const contentLen = blockContentText(block).length;
|
|
1606
2889
|
if (hasStart) {
|
|
1607
2890
|
const start = contentOffsetOf(block, range.startContainer, range.startOffset);
|
|
1608
|
-
return { start, length: Math.max(0, contentLen - start) };
|
|
2891
|
+
return trimmedSpan(block, { start, length: Math.max(0, contentLen - start) });
|
|
1609
2892
|
}
|
|
1610
2893
|
if (hasEnd) {
|
|
1611
2894
|
const end = contentOffsetOf(block, range.endContainer, range.endOffset);
|
|
1612
|
-
return { start: 0, length: end };
|
|
2895
|
+
return trimmedSpan(block, { start: 0, length: end });
|
|
1613
2896
|
}
|
|
1614
|
-
return { start: 0, length: contentLen };
|
|
2897
|
+
return trimmedSpan(block, { start: 0, length: contentLen });
|
|
1615
2898
|
}
|
|
1616
2899
|
/** Apply an inline ApplyFormat op to each block's slice of the selection, then reconcile
|
|
1617
2900
|
* the DOM incrementally (see {@link finishMultiBlockOp} — a full remount costs a whole-
|
|
@@ -1658,7 +2941,7 @@ var DocxodusEditor = (() => {
|
|
|
1658
2941
|
return {
|
|
1659
2942
|
block: b,
|
|
1660
2943
|
unid,
|
|
1661
|
-
fullId:
|
|
2944
|
+
fullId: this.anchorIdOf(b),
|
|
1662
2945
|
span: this.blockSpanForSelection(b),
|
|
1663
2946
|
res: null
|
|
1664
2947
|
};
|
|
@@ -1678,7 +2961,7 @@ var DocxodusEditor = (() => {
|
|
|
1678
2961
|
const edited = targets.filter((t) => t.res && t.unid);
|
|
1679
2962
|
if (edited.length === 0) return;
|
|
1680
2963
|
if (forceRemount || this.options.paginated || edited.some((t) => this.affectsList(t.res))) {
|
|
1681
|
-
this.
|
|
2964
|
+
this.refreshAfter(edited[0].block, -1, false);
|
|
1682
2965
|
return;
|
|
1683
2966
|
}
|
|
1684
2967
|
const swapped = [];
|
|
@@ -1721,7 +3004,7 @@ var DocxodusEditor = (() => {
|
|
|
1721
3004
|
}
|
|
1722
3005
|
const unid = block.getAttribute("data-anchor");
|
|
1723
3006
|
if (!unid) return;
|
|
1724
|
-
let fullId = this.
|
|
3007
|
+
let fullId = this.anchorIdOf(block);
|
|
1725
3008
|
if (!fullId) return;
|
|
1726
3009
|
const span = selectionSpanIn(block);
|
|
1727
3010
|
const on = value ?? !selectionHasFormat(key, block);
|
|
@@ -1737,7 +3020,7 @@ var DocxodusEditor = (() => {
|
|
|
1737
3020
|
);
|
|
1738
3021
|
if (!res.success) return;
|
|
1739
3022
|
if (this.affectsList(res)) {
|
|
1740
|
-
this.
|
|
3023
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1741
3024
|
return;
|
|
1742
3025
|
}
|
|
1743
3026
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1756,7 +3039,7 @@ var DocxodusEditor = (() => {
|
|
|
1756
3039
|
if (blocks.length > 1 && this.applyInlineOpAcrossBlocks(blocks, { fontSizePts: pts })) return;
|
|
1757
3040
|
const unid = block.getAttribute("data-anchor");
|
|
1758
3041
|
if (!unid) return;
|
|
1759
|
-
let fullId = this.
|
|
3042
|
+
let fullId = this.anchorIdOf(block);
|
|
1760
3043
|
if (!fullId) return;
|
|
1761
3044
|
let span = selectionSpanIn(block);
|
|
1762
3045
|
if (!span && this.lastSelection && this.lastSelection.unid === unid) span = this.lastSelection.span;
|
|
@@ -1771,7 +3054,7 @@ var DocxodusEditor = (() => {
|
|
|
1771
3054
|
);
|
|
1772
3055
|
if (!res.success) return;
|
|
1773
3056
|
if (this.affectsList(res)) {
|
|
1774
|
-
this.
|
|
3057
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1775
3058
|
return;
|
|
1776
3059
|
}
|
|
1777
3060
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1792,7 +3075,7 @@ var DocxodusEditor = (() => {
|
|
|
1792
3075
|
if (blocks.length > 1 && this.applyInlineOpAcrossBlocks(blocks, { fontFamily: name })) return;
|
|
1793
3076
|
const unid = block.getAttribute("data-anchor");
|
|
1794
3077
|
if (!unid) return;
|
|
1795
|
-
let fullId = this.
|
|
3078
|
+
let fullId = this.anchorIdOf(block);
|
|
1796
3079
|
if (!fullId) return;
|
|
1797
3080
|
let span = selectionSpanIn(block);
|
|
1798
3081
|
if (!span && this.lastSelection && this.lastSelection.unid === unid) span = this.lastSelection.span;
|
|
@@ -1807,7 +3090,7 @@ var DocxodusEditor = (() => {
|
|
|
1807
3090
|
);
|
|
1808
3091
|
if (!res.success) return;
|
|
1809
3092
|
if (this.affectsList(res)) {
|
|
1810
|
-
this.
|
|
3093
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1811
3094
|
return;
|
|
1812
3095
|
}
|
|
1813
3096
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1828,7 +3111,7 @@ var DocxodusEditor = (() => {
|
|
|
1828
3111
|
if (this.closed || !block) return;
|
|
1829
3112
|
const unid = block.getAttribute("data-anchor");
|
|
1830
3113
|
if (!unid) return;
|
|
1831
|
-
let fullId = this.
|
|
3114
|
+
let fullId = this.anchorIdOf(block);
|
|
1832
3115
|
if (!fullId) return;
|
|
1833
3116
|
const idx = this.blockIndex(block);
|
|
1834
3117
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -1841,7 +3124,13 @@ var DocxodusEditor = (() => {
|
|
|
1841
3124
|
)
|
|
1842
3125
|
);
|
|
1843
3126
|
if (!res.success) return;
|
|
1844
|
-
this.
|
|
3127
|
+
this.refreshAfter(
|
|
3128
|
+
block,
|
|
3129
|
+
idx,
|
|
3130
|
+
false,
|
|
3131
|
+
/* forceRemount */
|
|
3132
|
+
true
|
|
3133
|
+
);
|
|
1845
3134
|
}
|
|
1846
3135
|
/**
|
|
1847
3136
|
* Insert a `rows`×`cols` table after the active block. `options.cellContents` (row-major
|
|
@@ -1853,7 +3142,7 @@ var DocxodusEditor = (() => {
|
|
|
1853
3142
|
if (this.closed || !block) return;
|
|
1854
3143
|
const unid = block.getAttribute("data-anchor");
|
|
1855
3144
|
if (!unid) return;
|
|
1856
|
-
let fullId = this.
|
|
3145
|
+
let fullId = this.anchorIdOf(block);
|
|
1857
3146
|
if (!fullId) return;
|
|
1858
3147
|
const idx = this.blockIndex(block);
|
|
1859
3148
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -1869,7 +3158,7 @@ var DocxodusEditor = (() => {
|
|
|
1869
3158
|
)
|
|
1870
3159
|
);
|
|
1871
3160
|
if (!res.success) return;
|
|
1872
|
-
this.
|
|
3161
|
+
this.refreshAfter(block, idx, false);
|
|
1873
3162
|
}
|
|
1874
3163
|
// ─── Table row / column editing (active block must be inside a table cell) ──────────
|
|
1875
3164
|
/** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */
|
|
@@ -1878,13 +3167,13 @@ var DocxodusEditor = (() => {
|
|
|
1878
3167
|
if (this.closed || !block || !block.closest("table")) return;
|
|
1879
3168
|
const unid = block.getAttribute("data-anchor");
|
|
1880
3169
|
if (!unid) return;
|
|
1881
|
-
let fullId = this.
|
|
3170
|
+
let fullId = this.anchorIdOf(block);
|
|
1882
3171
|
if (!fullId) return;
|
|
1883
3172
|
const idx = this.blockIndex(block);
|
|
1884
3173
|
fullId = this.syncBlock(block, fullId);
|
|
1885
3174
|
const res = this.parseEdit(run(fullId));
|
|
1886
3175
|
if (!res.success) return;
|
|
1887
|
-
this.
|
|
3176
|
+
this.refreshAfter(block, idx, false);
|
|
1888
3177
|
}
|
|
1889
3178
|
/** Insert a row above/below the active cell's row. No-op outside a table. */
|
|
1890
3179
|
insertTableRow(where) {
|
|
@@ -1925,13 +3214,19 @@ var DocxodusEditor = (() => {
|
|
|
1925
3214
|
if (this.closed || !block) return;
|
|
1926
3215
|
const unid = block.getAttribute("data-anchor");
|
|
1927
3216
|
if (!unid) return;
|
|
1928
|
-
let fullId = this.
|
|
3217
|
+
let fullId = this.anchorIdOf(block);
|
|
1929
3218
|
if (!fullId) return;
|
|
1930
3219
|
const idx = this.blockIndex(block);
|
|
1931
3220
|
fullId = this.syncBlock(block, fullId);
|
|
1932
3221
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetListLevel(this.handle, fullId, delta));
|
|
1933
3222
|
if (!res.success) return;
|
|
1934
|
-
this.
|
|
3223
|
+
this.refreshAfter(
|
|
3224
|
+
block,
|
|
3225
|
+
idx,
|
|
3226
|
+
false,
|
|
3227
|
+
/* forceRemount */
|
|
3228
|
+
true
|
|
3229
|
+
);
|
|
1935
3230
|
}
|
|
1936
3231
|
/** Toggle (or set) page-break-before on the active block. */
|
|
1937
3232
|
pageBreakBefore(value = true) {
|
|
@@ -1946,7 +3241,7 @@ var DocxodusEditor = (() => {
|
|
|
1946
3241
|
if (this.closed || !block) return;
|
|
1947
3242
|
const unid = block.getAttribute("data-anchor");
|
|
1948
3243
|
if (!unid) return;
|
|
1949
|
-
let fullId = this.
|
|
3244
|
+
let fullId = this.anchorIdOf(block);
|
|
1950
3245
|
if (!fullId) return;
|
|
1951
3246
|
let membership = null;
|
|
1952
3247
|
try {
|
|
@@ -1960,7 +3255,13 @@ var DocxodusEditor = (() => {
|
|
|
1960
3255
|
this.exports.DocxSessionBridge.ApplyListFormat(this.handle, fullId, isThisKind ? "none" : kind)
|
|
1961
3256
|
);
|
|
1962
3257
|
if (!res.success) return;
|
|
1963
|
-
this.
|
|
3258
|
+
this.refreshAfter(
|
|
3259
|
+
block,
|
|
3260
|
+
idx,
|
|
3261
|
+
false,
|
|
3262
|
+
/* forceRemount */
|
|
3263
|
+
true
|
|
3264
|
+
);
|
|
1964
3265
|
}
|
|
1965
3266
|
/** Clear all paragraph borders (e.g. remove an inserted horizontal rule) on the active block —
|
|
1966
3267
|
* or every block in a multi-block selection. The engine/wire already accept `clearBorders`;
|
|
@@ -1982,12 +3283,46 @@ var DocxodusEditor = (() => {
|
|
|
1982
3283
|
if (this.editableList().length <= 1) return;
|
|
1983
3284
|
const unid = block.getAttribute("data-anchor");
|
|
1984
3285
|
if (!unid) return;
|
|
1985
|
-
const fullId = this.
|
|
3286
|
+
const fullId = this.anchorIdOf(block);
|
|
1986
3287
|
if (!fullId) return;
|
|
1987
3288
|
const idx = this.blockIndex(block);
|
|
1988
3289
|
const res = this.parseEdit(this.exports.DocxSessionBridge.DeleteBlock(this.handle, fullId));
|
|
1989
3290
|
if (!res.success) return;
|
|
1990
|
-
this.
|
|
3291
|
+
this.refreshAfter(block, Math.max(0, idx - 1), true);
|
|
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);
|
|
1991
3326
|
}
|
|
1992
3327
|
applyParagraphFormat(op) {
|
|
1993
3328
|
const block = this.activeBlock;
|
|
@@ -2002,7 +3337,7 @@ var DocxodusEditor = (() => {
|
|
|
2002
3337
|
return;
|
|
2003
3338
|
const unid = block.getAttribute("data-anchor");
|
|
2004
3339
|
if (!unid) return;
|
|
2005
|
-
let fullId = this.
|
|
3340
|
+
let fullId = this.anchorIdOf(block);
|
|
2006
3341
|
if (!fullId) return;
|
|
2007
3342
|
const idx = this.blockIndex(block);
|
|
2008
3343
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -2011,7 +3346,7 @@ var DocxodusEditor = (() => {
|
|
|
2011
3346
|
);
|
|
2012
3347
|
if (!res.success) return;
|
|
2013
3348
|
if (this.affectsList(res) || op.clearBorders) {
|
|
2014
|
-
this.
|
|
3349
|
+
this.refreshAfter(block, idx, false, true);
|
|
2015
3350
|
return;
|
|
2016
3351
|
}
|
|
2017
3352
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
@@ -2029,27 +3364,83 @@ var DocxodusEditor = (() => {
|
|
|
2029
3364
|
return;
|
|
2030
3365
|
const unid = block.getAttribute("data-anchor");
|
|
2031
3366
|
if (!unid) return;
|
|
2032
|
-
let fullId = this.
|
|
3367
|
+
let fullId = this.anchorIdOf(block);
|
|
2033
3368
|
if (!fullId) return;
|
|
2034
3369
|
const idx = this.blockIndex(block);
|
|
2035
3370
|
fullId = this.syncBlock(block, fullId);
|
|
2036
3371
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetParagraphStyle(this.handle, fullId, styleId));
|
|
2037
3372
|
if (!res.success) return;
|
|
2038
3373
|
if (this.affectsList(res)) {
|
|
2039
|
-
this.
|
|
3374
|
+
this.refreshAfter(block, idx, false, true);
|
|
2040
3375
|
return;
|
|
2041
3376
|
}
|
|
2042
3377
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
2043
3378
|
}
|
|
2044
|
-
/** Undo the last edit (re-
|
|
3379
|
+
/** Undo the last edit (incremental repaint; falls back to a full re-render). */
|
|
2045
3380
|
undo() {
|
|
2046
3381
|
if (this.closed) return;
|
|
2047
|
-
if (this.exports.DocxSessionBridge.Undo(this.handle)) this.
|
|
3382
|
+
if (this.exports.DocxSessionBridge.Undo(this.handle)) this.reconcile();
|
|
2048
3383
|
}
|
|
2049
|
-
/** Redo the last undone edit (re-
|
|
3384
|
+
/** Redo the last undone edit (incremental repaint; falls back to a full re-render). */
|
|
2050
3385
|
redo() {
|
|
2051
3386
|
if (this.closed) return;
|
|
2052
|
-
if (this.exports.DocxSessionBridge.Redo(this.handle)) this.
|
|
3387
|
+
if (this.exports.DocxSessionBridge.Redo(this.handle)) this.reconcile();
|
|
3388
|
+
}
|
|
3389
|
+
// ─── Header/footer region commands (no-ops unless `headerFooter` is on) ───────────────
|
|
3390
|
+
/**
|
|
3391
|
+
* Select which story kind a band edits (`"default"` / `"first"` / `"even"`). A kind with no
|
|
3392
|
+
* existing part is created empty, so the band always presents something editable. `"first"`
|
|
3393
|
+
* sets the section's `w:titlePg`; `"even"` sets the document-global `w:evenAndOddHeaders`
|
|
3394
|
+
* (which also governs footers — the band surfaces that caveat inline).
|
|
3395
|
+
*/
|
|
3396
|
+
setHeaderFooterKind(which, kind) {
|
|
3397
|
+
this.assertOpen();
|
|
3398
|
+
this.region?.setKind(which, kind);
|
|
3399
|
+
}
|
|
3400
|
+
/** The story kind a band is currently editing, or null when the region is off. */
|
|
3401
|
+
headerFooterKind(which) {
|
|
3402
|
+
return this.region?.kindOf(which) ?? null;
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Append a page-number field to the focused header/footer story paragraph (falling back to the
|
|
3406
|
+
* band's last paragraph — Word's convention). No-op outside a band.
|
|
3407
|
+
*/
|
|
3408
|
+
insertPageNumber(field = "currentPage") {
|
|
3409
|
+
this.assertOpen();
|
|
3410
|
+
if (!this.region) return;
|
|
3411
|
+
const block = this.activeBlock;
|
|
3412
|
+
const band = block ? this.region.bandOf(block) : null;
|
|
3413
|
+
const anchorId = block?.getAttribute("data-hf-anchor");
|
|
3414
|
+
if (band && anchorId) {
|
|
3415
|
+
this.region.insertPageNumber(this.region.whichOf(band), anchorId, field);
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
this.region.insertPageNumberInBand("footer", field);
|
|
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() ?? {};
|
|
2053
3444
|
}
|
|
2054
3445
|
/** Which inline formats the current selection carries — for ribbon button highlighting. */
|
|
2055
3446
|
queryFormatState() {
|
|
@@ -2066,15 +3457,20 @@ var DocxodusEditor = (() => {
|
|
|
2066
3457
|
}
|
|
2067
3458
|
/** Re-render one block from the live session by EditResult ref, swapping it in place. */
|
|
2068
3459
|
swapBlock(oldEl, oldUnid, ref) {
|
|
2069
|
-
const
|
|
3460
|
+
const inBand = this.isBandBlock(oldEl);
|
|
3461
|
+
const anchorId = ref?.id ?? this.anchorIdOf(oldEl);
|
|
2070
3462
|
const newUnid = ref?.unid ?? oldUnid;
|
|
2071
3463
|
if (!anchorId) return null;
|
|
2072
3464
|
const fresh = this.renderInto(anchorId);
|
|
2073
3465
|
if (!fresh || !this.replaceNode(oldEl, fresh)) return null;
|
|
2074
|
-
|
|
2075
|
-
|
|
3466
|
+
if (!inBand) {
|
|
3467
|
+
this.unidToFullId.delete(oldUnid);
|
|
3468
|
+
this.unidToFullId.set(newUnid, anchorId);
|
|
3469
|
+
}
|
|
3470
|
+
if (inBand) this.region.adoptBlock(fresh, anchorId);
|
|
2076
3471
|
this.wireBlock(fresh);
|
|
2077
3472
|
this.activeBlock = fresh;
|
|
3473
|
+
this.maybeRenumberNotes(fresh);
|
|
2078
3474
|
this.options.onEdit?.({ anchorId, unid: newUnid });
|
|
2079
3475
|
return fresh;
|
|
2080
3476
|
}
|
|
@@ -2097,17 +3493,19 @@ var DocxodusEditor = (() => {
|
|
|
2097
3493
|
);
|
|
2098
3494
|
if (html.charCodeAt(0) !== 123) return html;
|
|
2099
3495
|
}
|
|
2100
|
-
const bytes = bridge.Save(this.handle);
|
|
3496
|
+
const bytes = typeof bridge.SaveWithAnchorIds === "function" ? bridge.SaveWithAnchorIds(this.handle) : bridge.Save(this.handle);
|
|
2101
3497
|
return this.exports.DocumentConverter.ConvertDocxToHtmlComplete(
|
|
2102
3498
|
...completeArgs(bytes, this.options.cssPrefix, this.options.fabricateClasses, this.options.paginated, this.options.scale)
|
|
2103
3499
|
);
|
|
2104
3500
|
}
|
|
2105
|
-
/** Editable blocks in document order. */
|
|
3501
|
+
/** Editable BODY blocks in document order (band blocks are enumerated by `ownerRoot`). */
|
|
2106
3502
|
editableList() {
|
|
2107
3503
|
return Array.from(this.editRoot.querySelectorAll('[data-anchor][contenteditable="true"]'));
|
|
2108
3504
|
}
|
|
2109
3505
|
blockIndex(el) {
|
|
2110
|
-
return
|
|
3506
|
+
return Array.from(
|
|
3507
|
+
this.ownerRoot(el).querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
3508
|
+
).indexOf(el);
|
|
2111
3509
|
}
|
|
2112
3510
|
/**
|
|
2113
3511
|
* True when an edit produced or touched a list item (kind "li"). List markers and
|
|
@@ -2117,6 +3515,358 @@ var DocxodusEditor = (() => {
|
|
|
2117
3515
|
affectsList(res) {
|
|
2118
3516
|
return [...res.modified ?? [], ...res.created ?? []].some((r) => r.kind === "li");
|
|
2119
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
|
+
}
|
|
2120
3870
|
/**
|
|
2121
3871
|
* Full re-render from current session state (after undo/redo, and after list edits where
|
|
2122
3872
|
* single-block rendering can't compute numbering). Optionally focus the editable block at
|
|
@@ -2137,6 +3887,7 @@ var DocxodusEditor = (() => {
|
|
|
2137
3887
|
placeCaretAtOffset(target, caretAtEnd ? (target.textContent ?? "").length : 0);
|
|
2138
3888
|
}
|
|
2139
3889
|
}
|
|
3890
|
+
this.syncRegionToBody(this.activeBlock ?? void 0);
|
|
2140
3891
|
}
|
|
2141
3892
|
};
|
|
2142
3893
|
return __toCommonJS(editor_exports);
|