docxodus 9.9.0 → 10.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 +178 -0
- package/dist/canonical.d.ts +7 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +56 -0
- package/dist/canonical.js.map +1 -0
- package/dist/docxodus.worker.d.ts +2 -1
- package/dist/docxodus.worker.d.ts.map +1 -1
- package/dist/docxodus.worker.js +244 -3
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor.bundle.js +1914 -419
- package/dist/editor.d.ts +22 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +94 -9
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +2803 -469
- package/dist/embed.iife.js +2801 -467
- package/dist/export-assets.json +283 -0
- package/dist/export-browser.bundle.js +7990 -0
- package/dist/export-browser.d.ts +266 -0
- package/dist/export-browser.d.ts.map +1 -0
- package/dist/export-browser.js +2915 -0
- package/dist/export-browser.js.map +1 -0
- package/dist/export-resource-limits-v1.json +57 -0
- package/dist/font-contract.d.ts +150 -0
- package/dist/font-contract.d.ts.map +1 -0
- package/dist/font-contract.js +59 -0
- package/dist/font-contract.js.map +1 -0
- package/dist/font-runtime.d.ts +33 -0
- package/dist/font-runtime.d.ts.map +1 -0
- package/dist/font-runtime.js +1100 -0
- package/dist/font-runtime.js.map +1 -0
- package/dist/index.d.ts +62 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -16
- package/dist/index.js.map +1 -1
- package/dist/page-geometry.d.ts +8 -1
- package/dist/page-geometry.d.ts.map +1 -1
- package/dist/page-geometry.js +37 -11
- package/dist/page-geometry.js.map +1 -1
- package/dist/pagination.bundle.js +1827 -410
- package/dist/pagination.d.ts +249 -15
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +1942 -453
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +11 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +38 -5
- package/dist/react.js.map +1 -1
- package/dist/render-report-v2.schema.json +1581 -0
- package/dist/ribbon-chrome.d.ts +1 -1
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +45 -0
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon.js +70 -0
- package/dist/ribbon.js.map +1 -1
- package/dist/session.bundle.js +695 -29
- package/dist/session.d.ts +134 -19
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -25
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +1207 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +21 -21
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/worker-proxy.bundle.js +196 -29
- package/dist/worker-proxy.d.ts +36 -3
- package/dist/worker-proxy.d.ts.map +1 -1
- package/dist/worker-proxy.js +186 -33
- package/dist/worker-proxy.js.map +1 -1
- package/package.json +22 -6
package/dist/editor.bundle.js
CHANGED
|
@@ -172,17 +172,41 @@ var DocxodusEditor = (() => {
|
|
|
172
172
|
function ptToPx(pt) {
|
|
173
173
|
return pt / 0.75;
|
|
174
174
|
}
|
|
175
|
+
function finiteDataNumber(value, fallback) {
|
|
176
|
+
if (value === void 0 || value.trim() === "") return fallback;
|
|
177
|
+
const parsed = Number(value);
|
|
178
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
179
|
+
}
|
|
180
|
+
function positiveDataNumber(value, fallback) {
|
|
181
|
+
const parsed = finiteDataNumber(value, fallback);
|
|
182
|
+
return parsed > 0 ? parsed : fallback;
|
|
183
|
+
}
|
|
184
|
+
function nonNegativeDataNumber(value, fallback) {
|
|
185
|
+
const parsed = finiteDataNumber(value, fallback);
|
|
186
|
+
return parsed >= 0 ? parsed : fallback;
|
|
187
|
+
}
|
|
188
|
+
function positiveFinite(value, fallback) {
|
|
189
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
190
|
+
}
|
|
175
191
|
function parseSectionDimensions(section) {
|
|
176
|
-
const pageWidth =
|
|
177
|
-
const pageHeight =
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
192
|
+
const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
|
|
193
|
+
const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
|
|
194
|
+
const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
|
|
195
|
+
const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
|
|
196
|
+
const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
|
|
197
|
+
const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
|
|
198
|
+
const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
|
|
199
|
+
const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
|
|
200
|
+
const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
|
|
201
|
+
const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
|
|
202
|
+
const headerDistance = nonNegativeDataNumber(
|
|
203
|
+
section.dataset.headerHeight,
|
|
204
|
+
DEFAULT_HEADER_FOOTER_DISTANCE
|
|
205
|
+
);
|
|
206
|
+
const footerDistance = nonNegativeDataNumber(
|
|
207
|
+
section.dataset.footerHeight,
|
|
208
|
+
DEFAULT_HEADER_FOOTER_DISTANCE
|
|
209
|
+
);
|
|
186
210
|
return {
|
|
187
211
|
pageWidth,
|
|
188
212
|
pageHeight,
|
|
@@ -263,7 +287,8 @@ var DocxodusEditor = (() => {
|
|
|
263
287
|
return a.pageWidth === b.pageWidth && a.pageHeight === b.pageHeight && a.marginTop === b.marginTop && a.marginRight === b.marginRight && a.marginBottom === b.marginBottom && a.marginLeft === b.marginLeft;
|
|
264
288
|
}
|
|
265
289
|
var MAX_FOOTNOTE_AREA_RATIO = 0.6;
|
|
266
|
-
var
|
|
290
|
+
var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
|
|
291
|
+
var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
|
|
267
292
|
var PaginationEngine = class {
|
|
268
293
|
/**
|
|
269
294
|
* Creates a new pagination engine.
|
|
@@ -273,24 +298,46 @@ var DocxodusEditor = (() => {
|
|
|
273
298
|
* @param options - Pagination options
|
|
274
299
|
*/
|
|
275
300
|
constructor(staging, container, options = {}) {
|
|
301
|
+
this.createdPageCount = 0;
|
|
302
|
+
this.footnoteSeparator = null;
|
|
303
|
+
this.footnoteContinuationSeparator = null;
|
|
304
|
+
this.footnoteLayoutLikeWord8 = false;
|
|
276
305
|
this.pendingFootnoteContinuation = null;
|
|
277
306
|
/** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
|
|
278
307
|
this.pageNumbering = /* @__PURE__ */ new Map();
|
|
279
|
-
this.
|
|
280
|
-
this.
|
|
308
|
+
this.lastPages = [];
|
|
309
|
+
this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
|
|
310
|
+
this.state = "ready";
|
|
311
|
+
const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
|
|
312
|
+
this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
|
|
313
|
+
this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
|
|
281
314
|
if (!this.stagingElement) {
|
|
282
315
|
throw new Error("Staging element not found");
|
|
283
316
|
}
|
|
284
317
|
if (!this.containerElement) {
|
|
285
318
|
throw new Error("Container element not found");
|
|
286
319
|
}
|
|
320
|
+
if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
|
|
321
|
+
throw new Error("Staging and container elements must belong to the same document");
|
|
322
|
+
}
|
|
323
|
+
const view = ownerDocument.defaultView;
|
|
324
|
+
if (!view) {
|
|
325
|
+
throw new Error("Pagination requires an attached document with a defaultView");
|
|
326
|
+
}
|
|
327
|
+
this.document = ownerDocument;
|
|
328
|
+
this.view = view;
|
|
287
329
|
this.scale = options.scale ?? 1;
|
|
288
330
|
this.cssPrefix = options.cssPrefix ?? "page-";
|
|
289
331
|
this.showPageNumbers = options.showPageNumbers ?? true;
|
|
290
332
|
this.pageGap = options.pageGap ?? 20;
|
|
291
333
|
this.fragmentParagraphs = options.fragmentParagraphs ?? false;
|
|
334
|
+
this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
|
|
335
|
+
this.cancellationCheckpoint = options.checkCancellation;
|
|
336
|
+
this.pageCountCheckpoint = options.checkPageCount;
|
|
337
|
+
this.layoutToken = options.layoutToken;
|
|
292
338
|
this.hfRegistry = /* @__PURE__ */ new Map();
|
|
293
339
|
this.footnoteRegistry = /* @__PURE__ */ new Map();
|
|
340
|
+
this.commentMarginRegistry = /* @__PURE__ */ new Map();
|
|
294
341
|
}
|
|
295
342
|
/**
|
|
296
343
|
* Runs the pagination process.
|
|
@@ -298,58 +345,469 @@ var DocxodusEditor = (() => {
|
|
|
298
345
|
* @returns PaginationResult with page information
|
|
299
346
|
*/
|
|
300
347
|
paginate() {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
348
|
+
this.checkpoint();
|
|
349
|
+
if (this.state !== "ready") {
|
|
350
|
+
throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
|
|
351
|
+
}
|
|
352
|
+
if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
|
|
353
|
+
throw new Error("Pagination requires staging and container elements attached to their document");
|
|
354
|
+
}
|
|
355
|
+
if (this.document.documentElement.getBoundingClientRect().width <= 0) {
|
|
356
|
+
throw new Error("Pagination requires a browsing context with non-zero layout");
|
|
357
|
+
}
|
|
358
|
+
this.state = "running";
|
|
359
|
+
try {
|
|
360
|
+
const pages = [];
|
|
361
|
+
let pageNumber = 1;
|
|
362
|
+
this.hfRegistry = this.parseHeaderFooterRegistry();
|
|
363
|
+
this.footnoteRegistry = this.parseFootnoteRegistry();
|
|
364
|
+
({
|
|
365
|
+
normal: this.footnoteSeparator,
|
|
366
|
+
continuation: this.footnoteContinuationSeparator
|
|
367
|
+
} = this.parseFootnoteSeparators());
|
|
368
|
+
this.commentMarginRegistry = this.parseCommentMarginRegistry();
|
|
369
|
+
this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
|
|
370
|
+
const sections = this.stagingElement.querySelectorAll(
|
|
371
|
+
"[data-section-index]"
|
|
372
|
+
);
|
|
373
|
+
this.pageNumbering = this.parsePageNumbering(sections);
|
|
374
|
+
const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
|
|
375
|
+
this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
|
|
376
|
+
const referencedFootnoteIds = /* @__PURE__ */ new Set();
|
|
377
|
+
const referencedCommentIds = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const section of sectionsToProcess) {
|
|
379
|
+
this.checkpoint();
|
|
380
|
+
this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
|
|
381
|
+
for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
|
|
382
|
+
this.checkpoint();
|
|
383
|
+
if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
|
|
384
|
+
const id = reference.dataset.footnoteId;
|
|
385
|
+
if (id) referencedFootnoteIds.add(id);
|
|
386
|
+
}
|
|
387
|
+
for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
|
|
388
|
+
this.checkpoint();
|
|
389
|
+
if (reference.closest(
|
|
390
|
+
"#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
|
|
391
|
+
)) continue;
|
|
392
|
+
const id = reference.dataset.commentId;
|
|
393
|
+
if (id) referencedCommentIds.add(id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const id of referencedFootnoteIds) {
|
|
397
|
+
this.checkpoint();
|
|
398
|
+
const source = this.footnoteRegistry.get(id);
|
|
399
|
+
if (!source) continue;
|
|
400
|
+
this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
|
|
401
|
+
for (const reference of Array.from(
|
|
402
|
+
source.querySelectorAll("[data-comment-id]")
|
|
403
|
+
)) {
|
|
404
|
+
this.checkpoint();
|
|
405
|
+
const commentId = reference.dataset.commentId;
|
|
406
|
+
if (commentId) referencedCommentIds.add(commentId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const id of referencedCommentIds) {
|
|
410
|
+
this.checkpoint();
|
|
411
|
+
const source = this.commentMarginRegistry.get(id);
|
|
412
|
+
if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
|
|
413
|
+
}
|
|
414
|
+
const runs = [];
|
|
415
|
+
for (const section of sectionsToProcess) {
|
|
416
|
+
this.checkpoint();
|
|
417
|
+
const dims = parseSectionDimensions(section);
|
|
418
|
+
const previous = runs[runs.length - 1];
|
|
419
|
+
if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
|
|
420
|
+
previous.sections.push(section);
|
|
421
|
+
previous.sectionDimensions.set(
|
|
422
|
+
parseInt(section.dataset.sectionIndex || "0", 10),
|
|
423
|
+
dims
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
427
|
+
runs.push({
|
|
428
|
+
sections: [section],
|
|
429
|
+
sectionIndex,
|
|
430
|
+
sectionType: section.dataset.sectionType ?? "nextPage",
|
|
431
|
+
dims,
|
|
432
|
+
sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const run of runs) {
|
|
437
|
+
this.checkpoint();
|
|
438
|
+
const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
|
|
439
|
+
if (needsParityFiller) {
|
|
440
|
+
const precedingPage = pages[pages.length - 1];
|
|
441
|
+
const precedingPageInSection = parseInt(
|
|
442
|
+
precedingPage.element.dataset.pageInSection ?? "1",
|
|
443
|
+
10
|
|
444
|
+
);
|
|
445
|
+
const precedingDisplayedPageNumber = parseInt(
|
|
446
|
+
precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
|
|
447
|
+
10
|
|
448
|
+
);
|
|
449
|
+
const filler = this.createPage(
|
|
450
|
+
precedingPage.dimensions,
|
|
451
|
+
pageNumber,
|
|
452
|
+
precedingPage.sectionIndex,
|
|
453
|
+
precedingDisplayedPageNumber + 1,
|
|
454
|
+
[],
|
|
455
|
+
precedingPageInSection + 1,
|
|
456
|
+
[],
|
|
457
|
+
0,
|
|
458
|
+
null,
|
|
459
|
+
void 0,
|
|
460
|
+
true
|
|
461
|
+
);
|
|
462
|
+
pages.push(filler);
|
|
463
|
+
pageNumber++;
|
|
464
|
+
}
|
|
465
|
+
this.stagingElement.style.visibility = "hidden";
|
|
466
|
+
this.stagingElement.style.position = "absolute";
|
|
467
|
+
this.stagingElement.style.left = "-9999px";
|
|
468
|
+
this.stagingElement.style.display = "block";
|
|
469
|
+
const blocks = [];
|
|
470
|
+
for (const section of run.sections) {
|
|
471
|
+
this.checkpoint();
|
|
472
|
+
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
473
|
+
const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
|
|
474
|
+
section.style.width = `${sectionDims.contentWidth}pt`;
|
|
475
|
+
const columnCount = parseInt(section.dataset.cols || "1", 10);
|
|
476
|
+
if (columnCount > 1) {
|
|
477
|
+
const gap = parseFloat(section.dataset.colGap || "");
|
|
478
|
+
blocks.push(...this.buildColumnBlocks(
|
|
479
|
+
section,
|
|
480
|
+
sectionDims,
|
|
481
|
+
sectionIndex,
|
|
482
|
+
columnCount,
|
|
483
|
+
Number.isFinite(gap) ? gap : 36
|
|
484
|
+
));
|
|
485
|
+
} else {
|
|
486
|
+
blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const sectionPages = this.flowToPages(
|
|
490
|
+
blocks,
|
|
491
|
+
run.dims,
|
|
492
|
+
pageNumber,
|
|
493
|
+
run.sectionIndex,
|
|
494
|
+
run.sectionDimensions
|
|
495
|
+
);
|
|
496
|
+
this.checkpoint();
|
|
497
|
+
pages.push(...sectionPages);
|
|
498
|
+
pageNumber += sectionPages.length;
|
|
499
|
+
}
|
|
500
|
+
this.stagingElement.style.display = "none";
|
|
501
|
+
this.substitutePageNumberFields(pages.length);
|
|
502
|
+
for (const page of pages) {
|
|
503
|
+
this.checkpoint();
|
|
504
|
+
if (page.element.dataset.sectionFiller === "true") continue;
|
|
505
|
+
const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
|
|
506
|
+
const displayedPageNumber = parseInt(
|
|
507
|
+
page.element.dataset.displayedPageNumber || String(page.pageNumber),
|
|
508
|
+
10
|
|
509
|
+
);
|
|
510
|
+
const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
|
|
511
|
+
const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
|
|
512
|
+
if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
|
|
513
|
+
if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
|
|
514
|
+
}
|
|
515
|
+
this.qualifyPageFragments(pages);
|
|
516
|
+
this.transferVisibleFragmentTargets();
|
|
517
|
+
if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
|
|
518
|
+
this.lastPages = pages;
|
|
519
|
+
const result = {
|
|
520
|
+
totalPages: pages.length,
|
|
521
|
+
pages,
|
|
522
|
+
pageMap: this.layoutToken ? this.materializePageMap(
|
|
523
|
+
this.layoutToken.documentVersion,
|
|
524
|
+
this.layoutToken.rendererFingerprint
|
|
525
|
+
) : void 0
|
|
526
|
+
};
|
|
527
|
+
this.state = "complete";
|
|
528
|
+
return result;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
this.state = "failed";
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
checkpoint() {
|
|
535
|
+
this.cancellationCheckpoint?.();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Normalize visible fragment identities after a caller applies final standalone styles. This
|
|
539
|
+
* deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
|
|
540
|
+
* measurement cannot mutate a tree after it was declared stable.
|
|
541
|
+
*/
|
|
542
|
+
normalizePageMapFragmentIdentities() {
|
|
543
|
+
if (this.lastPages.length === 0) {
|
|
544
|
+
throw new Error("paginate() must complete before fragment identities can be normalized");
|
|
545
|
+
}
|
|
546
|
+
this.normalizeVisiblePageFragments(this.lastPages);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Materialize the last completed browser layout as portable page-relative point geometry.
|
|
550
|
+
* The caller supplies both invalidation tokens; this engine never guesses a document version
|
|
551
|
+
* or renderer fingerprint.
|
|
552
|
+
*/
|
|
553
|
+
materializePageMap(documentVersion, rendererFingerprint) {
|
|
554
|
+
if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
|
|
555
|
+
throw new Error("documentVersion must be a non-negative safe integer");
|
|
556
|
+
}
|
|
557
|
+
if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
|
|
558
|
+
if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
|
|
559
|
+
const pages = this.lastPages.map((page) => ({
|
|
560
|
+
pageNumber: page.pageNumber,
|
|
561
|
+
pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
|
|
562
|
+
width: page.dimensions.pageWidth,
|
|
563
|
+
height: page.dimensions.pageHeight,
|
|
564
|
+
sectionIndex: page.sectionIndex,
|
|
565
|
+
pageName: `docxodus-section-${page.sectionIndex}`
|
|
566
|
+
}));
|
|
567
|
+
const fragments = [];
|
|
568
|
+
const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
|
|
569
|
+
if (requiredAnchorIds.size === 0) {
|
|
570
|
+
throw new Error("cannot publish an available PageMap without canonical source inventory");
|
|
571
|
+
}
|
|
572
|
+
const measuredAnchorIds = /* @__PURE__ */ new Set();
|
|
573
|
+
const emittedFragmentCounts = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const page of this.lastPages) {
|
|
575
|
+
this.checkpoint();
|
|
576
|
+
const pageRect = page.element.getBoundingClientRect();
|
|
577
|
+
if (pageRect.width <= 0 || pageRect.height <= 0) {
|
|
578
|
+
throw new Error(`page ${page.pageNumber} has no measurable geometry`);
|
|
579
|
+
}
|
|
580
|
+
const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
|
|
581
|
+
const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
|
|
582
|
+
const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
|
|
583
|
+
for (const element of Array.from(nodes)) {
|
|
584
|
+
this.checkpoint();
|
|
585
|
+
if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
|
|
586
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
587
|
+
if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
|
|
588
|
+
throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
|
|
589
|
+
}
|
|
590
|
+
const rect = element.getBoundingClientRect();
|
|
591
|
+
const style = this.view.getComputedStyle(element);
|
|
592
|
+
const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
|
|
593
|
+
if (deliberatelyHidden) continue;
|
|
594
|
+
requiredAnchorIds.add(anchorId);
|
|
595
|
+
const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
|
|
596
|
+
const left2 = visibleRect.left;
|
|
597
|
+
const top2 = visibleRect.top;
|
|
598
|
+
const right2 = visibleRect.right;
|
|
599
|
+
const bottom2 = visibleRect.bottom;
|
|
600
|
+
if (rect.width <= 0 || rect.height <= 0 || right2 <= left2 || bottom2 <= top2) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
measuredAnchorIds.add(anchorId);
|
|
604
|
+
const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
|
|
605
|
+
emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
606
|
+
const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
607
|
+
if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`page ${page.pageNumber} fragment identity changed after final-tree normalization`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
fragments.push({
|
|
613
|
+
fragmentId,
|
|
614
|
+
anchorId,
|
|
615
|
+
fragmentIndex,
|
|
616
|
+
pageNumber: page.pageNumber,
|
|
617
|
+
geometry: {
|
|
618
|
+
x: (left2 - pageRect.left) * pointPerRenderedX,
|
|
619
|
+
y: (top2 - pageRect.top) * pointPerRenderedY,
|
|
620
|
+
width: (right2 - left2) * pointPerRenderedX,
|
|
621
|
+
height: (bottom2 - top2) * pointPerRenderedY
|
|
622
|
+
},
|
|
623
|
+
story: this.storyForCanonicalAnchor(anchorId),
|
|
624
|
+
inTableCell: element.matches("td,th") || element.closest("td,th") !== null
|
|
321
625
|
});
|
|
322
626
|
}
|
|
323
627
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
628
|
+
const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
|
|
629
|
+
if (missingAnchor) {
|
|
630
|
+
throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
schemaVersion: 1,
|
|
634
|
+
mode: "paginated",
|
|
635
|
+
availability: "available",
|
|
636
|
+
documentVersion,
|
|
637
|
+
rendererFingerprint,
|
|
638
|
+
pages,
|
|
639
|
+
fragments
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
normalizeVisiblePageFragments(pages) {
|
|
643
|
+
const emittedFragmentCounts = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const page of pages) {
|
|
645
|
+
this.checkpoint();
|
|
646
|
+
const pageRect = page.element.getBoundingClientRect();
|
|
647
|
+
for (const element of Array.from(
|
|
648
|
+
page.element.querySelectorAll("[data-source-anchor-id]")
|
|
649
|
+
)) {
|
|
650
|
+
this.checkpoint();
|
|
651
|
+
if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
|
|
652
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
653
|
+
if (!anchorId) continue;
|
|
654
|
+
const style = this.view.getComputedStyle(element);
|
|
655
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
656
|
+
const rect = element.getBoundingClientRect();
|
|
657
|
+
const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
|
|
658
|
+
if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
|
|
659
|
+
const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
|
|
660
|
+
emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
661
|
+
element.dataset.pageNumber = String(page.pageNumber);
|
|
662
|
+
element.dataset.fragmentIndex = String(fragmentIndex);
|
|
663
|
+
element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Intersect an element with every ancestor that establishes an overflow clip before the page
|
|
669
|
+
* root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
|
|
670
|
+
* therefore must not satisfy PageMap completeness or inflate portable geometry.
|
|
671
|
+
*/
|
|
672
|
+
intersectWithClippingAncestors(element, page, pageRect, rect) {
|
|
673
|
+
let left2 = Math.max(rect.left, pageRect.left);
|
|
674
|
+
let top2 = Math.max(rect.top, pageRect.top);
|
|
675
|
+
let right2 = Math.min(rect.right, pageRect.right);
|
|
676
|
+
let bottom2 = Math.min(rect.bottom, pageRect.bottom);
|
|
677
|
+
const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
|
|
678
|
+
for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
|
|
679
|
+
const style = this.view.getComputedStyle(ancestor);
|
|
680
|
+
const clipsX = clips(style.overflowX);
|
|
681
|
+
const clipsY = clips(style.overflowY);
|
|
682
|
+
if (!clipsX && !clipsY) continue;
|
|
683
|
+
const ancestorRect = ancestor.getBoundingClientRect();
|
|
684
|
+
if (clipsX) {
|
|
685
|
+
left2 = Math.max(left2, ancestorRect.left);
|
|
686
|
+
right2 = Math.min(right2, ancestorRect.right);
|
|
687
|
+
}
|
|
688
|
+
if (clipsY) {
|
|
689
|
+
top2 = Math.max(top2, ancestorRect.top);
|
|
690
|
+
bottom2 = Math.min(bottom2, ancestorRect.bottom);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return { left: left2, top: top2, right: right2, bottom: bottom2 };
|
|
694
|
+
}
|
|
695
|
+
storyForCanonicalAnchor(anchorId) {
|
|
696
|
+
const first = anchorId.indexOf(":");
|
|
697
|
+
const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
|
|
698
|
+
const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
|
|
699
|
+
if (scope.startsWith("hdr")) return "header";
|
|
700
|
+
if (scope.startsWith("ftr")) return "footer";
|
|
701
|
+
if (scope === "fn") return "footnote";
|
|
702
|
+
if (scope === "en") return "endnote";
|
|
703
|
+
if (scope === "cmt") return "comment";
|
|
704
|
+
return "body";
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
|
|
708
|
+
* Registry wrappers are excluded when scanning staging because selectable registry contents are
|
|
709
|
+
* inventoried separately. Producers may explicitly mark content that has no visual substrate
|
|
710
|
+
* with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
|
|
711
|
+
*/
|
|
712
|
+
collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
|
|
713
|
+
const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
|
|
714
|
+
candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
|
|
715
|
+
for (const element of candidates) {
|
|
716
|
+
if (excludeRegistries && element.closest(
|
|
717
|
+
"#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
|
|
718
|
+
)) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (this.isZeroHeightExplicitBreakCarrier(element)) {
|
|
722
|
+
element.dataset.pageMapExclude = "true";
|
|
723
|
+
}
|
|
724
|
+
if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
|
|
725
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
726
|
+
if (anchorId) destination.add(anchorId);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
isZeroHeightExplicitBreakCarrier(element) {
|
|
730
|
+
const next = element.nextElementSibling;
|
|
731
|
+
const bounds = element.getBoundingClientRect();
|
|
732
|
+
return element.hasAttribute("data-source-anchor-id") && (element.textContent ?? "").replace(/\u00a0/g, "").trim() === "" && bounds.height <= 0.01 && !element.querySelector("[data-source-anchor-id]") && !element.querySelector("img,svg,canvas,table,hr,input,textarea,select") && (next?.dataset.pageBreak === "true" || next?.classList.contains(`${this.cssPrefix}break`) === true);
|
|
733
|
+
}
|
|
734
|
+
isDeliberatelyUnrenderedSource(element, sourceRoot) {
|
|
735
|
+
for (let current = element; current; current = current.parentElement) {
|
|
736
|
+
const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
|
|
737
|
+
if (current.dataset.pageMapExclude === "true" || current.dataset.pageBreak === "true" || current.classList.contains(`${this.cssPrefix}break`) || zeroHeightExplicitBreakCarrier || current.hidden || current.getAttribute("aria-hidden") === "true" || current.style.display === "none" || current.style.visibility === "hidden") {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
if (current === sourceRoot) break;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
|
|
746
|
+
* canonical source identity plus page/fragment qualification instead.
|
|
747
|
+
*/
|
|
748
|
+
qualifyPageFragments(pages) {
|
|
749
|
+
const fragmentCounts = /* @__PURE__ */ new Map();
|
|
750
|
+
const activeCanonicalIds = /* @__PURE__ */ new Set();
|
|
751
|
+
const activeBareAnchorIds = /* @__PURE__ */ new Set();
|
|
752
|
+
const makeInactive = (element) => {
|
|
753
|
+
element.removeAttribute("data-anchor");
|
|
754
|
+
element.removeAttribute("data-committed-text");
|
|
755
|
+
if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
|
|
756
|
+
};
|
|
757
|
+
for (const page of pages) {
|
|
758
|
+
const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
|
|
759
|
+
for (const element of Array.from(nodes)) {
|
|
760
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
761
|
+
if (!anchorId) continue;
|
|
762
|
+
const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
|
|
763
|
+
fragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
764
|
+
element.dataset.pageNumber = String(page.pageNumber);
|
|
765
|
+
element.dataset.fragmentIndex = String(fragmentIndex);
|
|
766
|
+
element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
767
|
+
const story = this.storyForCanonicalAnchor(anchorId);
|
|
768
|
+
const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
|
|
769
|
+
if (element.hasAttribute("data-anchor")) {
|
|
770
|
+
const bareAnchorId = element.dataset.anchor;
|
|
771
|
+
if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
|
|
772
|
+
activeCanonicalIds.add(anchorId);
|
|
773
|
+
activeBareAnchorIds.add(bareAnchorId);
|
|
774
|
+
} else {
|
|
775
|
+
makeInactive(element);
|
|
776
|
+
}
|
|
344
777
|
}
|
|
345
778
|
}
|
|
346
|
-
const sectionPages = this.flowToPages(blocks, run.dims, pageNumber, run.sectionIndex);
|
|
347
|
-
pages.push(...sectionPages);
|
|
348
|
-
pageNumber += sectionPages.length;
|
|
349
779
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
780
|
+
const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
|
|
781
|
+
for (const element of Array.from(
|
|
782
|
+
this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
|
|
783
|
+
)) {
|
|
784
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
785
|
+
if (!anchorId) continue;
|
|
786
|
+
const bareAnchorId = element.dataset.anchor;
|
|
787
|
+
if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
|
|
788
|
+
makeInactive(element);
|
|
789
|
+
} else {
|
|
790
|
+
activeStagingCanonicalIds.add(anchorId);
|
|
791
|
+
activeBareAnchorIds.add(bareAnchorId);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
|
|
797
|
+
* fragment target copied into a visible page would therefore resolve to its earlier hidden
|
|
798
|
+
* source. Transfer target ownership to the page presentation after flow is complete; registry
|
|
799
|
+
* and wrapper IDs that have no visible counterpart remain available to pagination internals.
|
|
800
|
+
*/
|
|
801
|
+
transferVisibleFragmentTargets() {
|
|
802
|
+
const visibleIds = new Set(Array.from(
|
|
803
|
+
this.containerElement.querySelectorAll("[id]")
|
|
804
|
+
).map((element) => element.id).filter(Boolean));
|
|
805
|
+
if (visibleIds.size === 0) return;
|
|
806
|
+
for (const source of Array.from(
|
|
807
|
+
this.stagingElement.querySelectorAll("[id]")
|
|
808
|
+
)) {
|
|
809
|
+
if (visibleIds.has(source.id)) source.removeAttribute("id");
|
|
810
|
+
}
|
|
353
811
|
}
|
|
354
812
|
/** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
|
|
355
813
|
parsePageNumbering(sections) {
|
|
@@ -393,9 +851,8 @@ var DocxodusEditor = (() => {
|
|
|
393
851
|
if (markers.length === 0) continue;
|
|
394
852
|
const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
|
|
395
853
|
const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
|
|
396
|
-
const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
|
|
397
854
|
const numbering = this.pageNumbering.get(sectionIndex) ?? {};
|
|
398
|
-
const displayed =
|
|
855
|
+
const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
|
|
399
856
|
for (const marker of Array.from(markers)) {
|
|
400
857
|
const kind = marker.dataset.field;
|
|
401
858
|
if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
|
|
@@ -407,17 +864,26 @@ var DocxodusEditor = (() => {
|
|
|
407
864
|
/**
|
|
408
865
|
* Measures all content blocks in a section.
|
|
409
866
|
*/
|
|
410
|
-
measureBlocks(section, dims) {
|
|
867
|
+
measureBlocks(section, dims, sectionIndex) {
|
|
411
868
|
const blocks = [];
|
|
412
869
|
const children = Array.from(section.children);
|
|
413
870
|
for (const child of children) {
|
|
871
|
+
this.checkpoint();
|
|
414
872
|
if (child.dataset.sectionIndex !== void 0) {
|
|
415
|
-
const
|
|
873
|
+
const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
|
|
874
|
+
const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
|
|
416
875
|
blocks.push(...nestedBlocks);
|
|
417
876
|
continue;
|
|
418
877
|
}
|
|
878
|
+
if (child.matches("section.endnotes")) {
|
|
879
|
+
const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
|
|
880
|
+
if (endnoteBlocks) {
|
|
881
|
+
blocks.push(...endnoteBlocks);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
419
885
|
const rect = child.getBoundingClientRect();
|
|
420
|
-
const style =
|
|
886
|
+
const style = this.view.getComputedStyle(child);
|
|
421
887
|
const marginTopPx = parseFloat(style.marginTop) || 0;
|
|
422
888
|
const marginBottomPx = parseFloat(style.marginBottom) || 0;
|
|
423
889
|
const heightPt = pxToPt(rect.height);
|
|
@@ -426,6 +892,7 @@ var DocxodusEditor = (() => {
|
|
|
426
892
|
const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
|
|
427
893
|
blocks.push({
|
|
428
894
|
element: child,
|
|
895
|
+
sectionIndex,
|
|
429
896
|
heightPt,
|
|
430
897
|
marginTopPt,
|
|
431
898
|
marginBottomPt,
|
|
@@ -438,6 +905,95 @@ var DocxodusEditor = (() => {
|
|
|
438
905
|
}
|
|
439
906
|
return blocks;
|
|
440
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Flatten the converter's `section.endnotes > ol > li > p` presentation into
|
|
910
|
+
* ordinary paragraph blocks. This preserves paragraph formatting and canonical
|
|
911
|
+
* p:en/en:en identities while allowing the existing paragraph fragmenter to
|
|
912
|
+
* split a long endnote across page boundaries.
|
|
913
|
+
*/
|
|
914
|
+
measureSafeEndnoteBlocks(section, dims, sectionIndex) {
|
|
915
|
+
const sectionChildren = Array.from(section.children);
|
|
916
|
+
const list = sectionChildren.find((child) => child.tagName === "OL");
|
|
917
|
+
if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const items = Array.from(list.children);
|
|
921
|
+
if (items.length === 0 || items.some(
|
|
922
|
+
(item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
|
|
923
|
+
)) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const blocks = [];
|
|
927
|
+
const sectionStyle = this.view.getComputedStyle(section);
|
|
928
|
+
for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
|
|
929
|
+
this.checkpoint();
|
|
930
|
+
const clonedRule = rule.cloneNode(true);
|
|
931
|
+
if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
|
|
932
|
+
blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
|
|
933
|
+
}
|
|
934
|
+
const listStyle = this.view.getComputedStyle(list);
|
|
935
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
936
|
+
this.checkpoint();
|
|
937
|
+
const item = items[itemIndex];
|
|
938
|
+
const ownerAnchorId = item.dataset.sourceAnchorId;
|
|
939
|
+
const paragraphs = Array.from(item.children);
|
|
940
|
+
for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
|
|
941
|
+
this.checkpoint();
|
|
942
|
+
const paragraph = paragraphs[paragraphIndex];
|
|
943
|
+
if (!this.hasRangeFragmentSafeLayout(paragraph)) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
const clone = paragraph.cloneNode(true);
|
|
947
|
+
clone.dataset.paginationSafeEndnote = "true";
|
|
948
|
+
clone.style.fontSize ||= sectionStyle.fontSize;
|
|
949
|
+
clone.style.lineHeight ||= sectionStyle.lineHeight;
|
|
950
|
+
clone.style.paddingLeft ||= listStyle.paddingLeft;
|
|
951
|
+
if (ownerAnchorId && !Array.from(
|
|
952
|
+
clone.querySelectorAll("[data-source-anchor-id]")
|
|
953
|
+
).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
|
|
954
|
+
const owner = this.document.createElement("span");
|
|
955
|
+
owner.dataset.sourceAnchorId = ownerAnchorId;
|
|
956
|
+
while (clone.firstChild) owner.appendChild(clone.firstChild);
|
|
957
|
+
clone.appendChild(owner);
|
|
958
|
+
}
|
|
959
|
+
if (paragraphIndex === 0) {
|
|
960
|
+
const itemId = item.id;
|
|
961
|
+
if (itemId) {
|
|
962
|
+
if (clone.id && clone.id !== itemId) return null;
|
|
963
|
+
clone.id = itemId;
|
|
964
|
+
}
|
|
965
|
+
const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
|
|
966
|
+
const marker = this.formatOrderedListMarker(
|
|
967
|
+
Number.isFinite(value) ? value : itemIndex + 1,
|
|
968
|
+
listStyle.listStyleType
|
|
969
|
+
);
|
|
970
|
+
clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
|
|
971
|
+
}
|
|
972
|
+
blocks.push(this.measureElement(clone, dims, sectionIndex));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return blocks;
|
|
976
|
+
}
|
|
977
|
+
/** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
|
|
978
|
+
formatOrderedListMarker(value, listStyleType) {
|
|
979
|
+
const format = (() => {
|
|
980
|
+
switch (listStyleType) {
|
|
981
|
+
case "lower-roman":
|
|
982
|
+
return "lowerRoman";
|
|
983
|
+
case "upper-roman":
|
|
984
|
+
return "upperRoman";
|
|
985
|
+
case "lower-alpha":
|
|
986
|
+
case "lower-latin":
|
|
987
|
+
return "lowerLetter";
|
|
988
|
+
case "upper-alpha":
|
|
989
|
+
case "upper-latin":
|
|
990
|
+
return "upperLetter";
|
|
991
|
+
default:
|
|
992
|
+
return "decimal";
|
|
993
|
+
}
|
|
994
|
+
})();
|
|
995
|
+
return formatPageNumber(value, format);
|
|
996
|
+
}
|
|
441
997
|
/**
|
|
442
998
|
* Flows a multi-column (`w:cols`) section's children into CSS-multicol container
|
|
443
999
|
* blocks. Word lays such a section out as N columns inside the same body extent;
|
|
@@ -455,7 +1011,7 @@ var DocxodusEditor = (() => {
|
|
|
455
1011
|
const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
456
1012
|
const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
|
|
457
1013
|
const makeContainer = (slice) => {
|
|
458
|
-
const container = document.createElement("div");
|
|
1014
|
+
const container = this.document.createElement("div");
|
|
459
1015
|
container.style.columnCount = String(columnCount);
|
|
460
1016
|
container.style.columnGap = `${columnGapPt}pt`;
|
|
461
1017
|
for (const child of slice) {
|
|
@@ -465,17 +1021,19 @@ var DocxodusEditor = (() => {
|
|
|
465
1021
|
};
|
|
466
1022
|
let start2 = 0;
|
|
467
1023
|
while (start2 < children.length) {
|
|
1024
|
+
this.checkpoint();
|
|
468
1025
|
if (isBreak(children[start2])) {
|
|
469
|
-
blocks.push(this.measureElement(children[start2], dims));
|
|
1026
|
+
blocks.push(this.measureElement(children[start2], dims, sectionIndex));
|
|
470
1027
|
start2++;
|
|
471
1028
|
continue;
|
|
472
1029
|
}
|
|
473
1030
|
let end = start2 + 1;
|
|
474
1031
|
let container = makeContainer(children.slice(start2, end));
|
|
475
|
-
let measured = this.measureElement(container, dims);
|
|
1032
|
+
let measured = this.measureElement(container, dims, sectionIndex);
|
|
476
1033
|
while (end < children.length && !isBreak(children[end])) {
|
|
1034
|
+
this.checkpoint();
|
|
477
1035
|
const candidate = makeContainer(children.slice(start2, end + 1));
|
|
478
|
-
const candidateMeasured = this.measureElement(candidate, dims);
|
|
1036
|
+
const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
|
|
479
1037
|
if (candidateMeasured.heightPt > maxFragmentHeight) break;
|
|
480
1038
|
container = candidate;
|
|
481
1039
|
measured = candidateMeasured;
|
|
@@ -483,6 +1041,7 @@ var DocxodusEditor = (() => {
|
|
|
483
1041
|
}
|
|
484
1042
|
blocks.push({
|
|
485
1043
|
element: container,
|
|
1044
|
+
sectionIndex,
|
|
486
1045
|
heightPt: measured.heightPt,
|
|
487
1046
|
marginTopPt: measured.marginTopPt,
|
|
488
1047
|
marginBottomPt: measured.marginBottomPt,
|
|
@@ -501,8 +1060,8 @@ var DocxodusEditor = (() => {
|
|
|
501
1060
|
* This is intentionally DOM-based: table row heights cannot be inferred from individual
|
|
502
1061
|
* rows because wrapping and collapsed borders change the height of a fragment.
|
|
503
1062
|
*/
|
|
504
|
-
measureElement(element, dims) {
|
|
505
|
-
const measurementHost = document.createElement("div");
|
|
1063
|
+
measureElement(element, dims, sectionIndex) {
|
|
1064
|
+
const measurementHost = this.document.createElement("div");
|
|
506
1065
|
measurementHost.style.position = "absolute";
|
|
507
1066
|
measurementHost.style.visibility = "hidden";
|
|
508
1067
|
measurementHost.style.left = "-9999px";
|
|
@@ -511,9 +1070,10 @@ var DocxodusEditor = (() => {
|
|
|
511
1070
|
measurementHost.appendChild(measuredElement);
|
|
512
1071
|
this.stagingElement.appendChild(measurementHost);
|
|
513
1072
|
const rect = measuredElement.getBoundingClientRect();
|
|
514
|
-
const style =
|
|
1073
|
+
const style = this.view.getComputedStyle(measuredElement);
|
|
515
1074
|
const measured = {
|
|
516
1075
|
element,
|
|
1076
|
+
sectionIndex,
|
|
517
1077
|
heightPt: pxToPt(rect.height),
|
|
518
1078
|
marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
|
|
519
1079
|
marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
|
|
@@ -607,6 +1167,7 @@ var DocxodusEditor = (() => {
|
|
|
607
1167
|
const knownIds = new Set(existingFootnoteIds);
|
|
608
1168
|
const newIds = [];
|
|
609
1169
|
for (const block of blocks) {
|
|
1170
|
+
this.checkpoint();
|
|
610
1171
|
for (const id of this.extractFootnoteRefs(block.element)) {
|
|
611
1172
|
if (!knownIds.has(id)) {
|
|
612
1173
|
knownIds.add(id);
|
|
@@ -624,8 +1185,8 @@ var DocxodusEditor = (() => {
|
|
|
624
1185
|
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
625
1186
|
return Math.min(
|
|
626
1187
|
this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
|
|
627
|
-
this.getPageBands(dims, sectionIndex, 2,
|
|
628
|
-
this.getPageBands(dims, sectionIndex,
|
|
1188
|
+
this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
|
|
1189
|
+
this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
|
|
629
1190
|
);
|
|
630
1191
|
}
|
|
631
1192
|
/**
|
|
@@ -663,10 +1224,11 @@ var DocxodusEditor = (() => {
|
|
|
663
1224
|
if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
|
|
664
1225
|
return null;
|
|
665
1226
|
}
|
|
666
|
-
const
|
|
667
|
-
if (!
|
|
1227
|
+
const tableElement = wrapper.firstElementChild;
|
|
1228
|
+
if (!tableElement || tableElement.localName !== "table") {
|
|
668
1229
|
return null;
|
|
669
1230
|
}
|
|
1231
|
+
const table = tableElement;
|
|
670
1232
|
const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
671
1233
|
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]")) {
|
|
672
1234
|
return null;
|
|
@@ -680,8 +1242,10 @@ var DocxodusEditor = (() => {
|
|
|
680
1242
|
const groups = [];
|
|
681
1243
|
let start2 = 0;
|
|
682
1244
|
while (start2 < rows.length) {
|
|
1245
|
+
this.checkpoint();
|
|
683
1246
|
let end = start2;
|
|
684
1247
|
while (end < rows.length) {
|
|
1248
|
+
this.checkpoint();
|
|
685
1249
|
const candidate = this.createSimpleTableFragment(
|
|
686
1250
|
wrapper,
|
|
687
1251
|
table,
|
|
@@ -689,7 +1253,7 @@ var DocxodusEditor = (() => {
|
|
|
689
1253
|
rows.slice(start2, end + 1),
|
|
690
1254
|
start2 === 0
|
|
691
1255
|
);
|
|
692
|
-
const measured = this.measureElement(candidate, dims);
|
|
1256
|
+
const measured = this.measureElement(candidate, dims, block.sectionIndex);
|
|
693
1257
|
if (measured.heightPt > maximumFragmentHeight) {
|
|
694
1258
|
break;
|
|
695
1259
|
}
|
|
@@ -721,7 +1285,7 @@ var DocxodusEditor = (() => {
|
|
|
721
1285
|
if (!isLast) {
|
|
722
1286
|
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
723
1287
|
}
|
|
724
|
-
const measured = this.measureElement(fragment, dims);
|
|
1288
|
+
const measured = this.measureElement(fragment, dims, block.sectionIndex);
|
|
725
1289
|
if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
|
|
726
1290
|
return null;
|
|
727
1291
|
}
|
|
@@ -736,35 +1300,198 @@ var DocxodusEditor = (() => {
|
|
|
736
1300
|
return fragments;
|
|
737
1301
|
}
|
|
738
1302
|
/**
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
1303
|
+
* DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
|
|
1304
|
+
* offsets are checked against the browser's Unicode grapheme segmenter, so a
|
|
1305
|
+
* formatting-run boundary can never bisect a surrogate pair, combining
|
|
1306
|
+
* sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
|
|
1307
|
+
* PAGE/NUMPAGES field results are atomic because splitting their marker would
|
|
1308
|
+
* make later substitution duplicate or replace only half of the field.
|
|
742
1309
|
*/
|
|
743
|
-
paragraphFragmentEndpoints(paragraph) {
|
|
744
|
-
const
|
|
745
|
-
const
|
|
1310
|
+
paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
|
|
1311
|
+
const textNodes = [];
|
|
1312
|
+
const textStarts = /* @__PURE__ */ new Map();
|
|
1313
|
+
const lastTextByField = /* @__PURE__ */ new Map();
|
|
1314
|
+
const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
|
|
1315
|
+
const flattenedChunks = [];
|
|
1316
|
+
let flattenedLength = 0;
|
|
746
1317
|
let textNode;
|
|
747
1318
|
while (textNode = walker.nextNode()) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1319
|
+
this.checkpoint();
|
|
1320
|
+
textStarts.set(textNode, flattenedLength);
|
|
1321
|
+
textNodes.push(textNode);
|
|
1322
|
+
flattenedChunks.push(textNode.data);
|
|
1323
|
+
flattenedLength += textNode.data.length;
|
|
1324
|
+
let field = textNode.parentElement?.closest("[data-field]") ?? null;
|
|
1325
|
+
while (field?.parentElement?.closest("[data-field]")) {
|
|
1326
|
+
field = field.parentElement.closest("[data-field]");
|
|
1327
|
+
}
|
|
1328
|
+
if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
|
|
1329
|
+
}
|
|
1330
|
+
const flattenedText = flattenedChunks.join("");
|
|
1331
|
+
if (!this.hasVisibleText(flattenedText)) return [];
|
|
1332
|
+
const invisible = /[\s\u200B-\u200F\uFEFF]/;
|
|
1333
|
+
let firstVisibleOffset = -1;
|
|
1334
|
+
let lastVisibleOffset = -1;
|
|
1335
|
+
for (let index = 0; index < flattenedText.length; index++) {
|
|
1336
|
+
if (index % 4096 === 0) this.checkpoint();
|
|
1337
|
+
if (invisible.test(flattenedText[index])) continue;
|
|
1338
|
+
if (firstVisibleOffset < 0) firstVisibleOffset = index;
|
|
1339
|
+
lastVisibleOffset = index;
|
|
1340
|
+
}
|
|
1341
|
+
const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
|
|
1342
|
+
const candidates = [];
|
|
1343
|
+
const addCandidate = (candidate) => {
|
|
1344
|
+
const { textOffset } = candidate;
|
|
1345
|
+
if (textOffset <= 0 || textOffset >= flattenedText.length) return;
|
|
1346
|
+
if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
|
|
1347
|
+
if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
|
|
1348
|
+
if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
|
|
1349
|
+
if (graphemeBoundaries) {
|
|
1350
|
+
if (!graphemeBoundaries.has(textOffset)) return;
|
|
1351
|
+
} else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
candidates.push(candidate);
|
|
1355
|
+
};
|
|
1356
|
+
for (const node of textNodes) {
|
|
1357
|
+
this.checkpoint();
|
|
1358
|
+
const start2 = textStarts.get(node);
|
|
1359
|
+
const atomic = node.parentElement?.closest(
|
|
1360
|
+
"[data-field], [data-list-marker], a[data-comment-id]"
|
|
1361
|
+
);
|
|
1362
|
+
if (atomic && paragraph.contains(atomic)) continue;
|
|
1363
|
+
const whitespace = /[\u0009-\u000D\u0020]+/g;
|
|
751
1364
|
let match;
|
|
752
|
-
|
|
753
|
-
|
|
1365
|
+
let matchesSinceCheckpoint = 0;
|
|
1366
|
+
while ((match = whitespace.exec(node.data)) !== null) {
|
|
1367
|
+
if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
|
|
1368
|
+
const offset = match.index + match[0].length;
|
|
1369
|
+
addCandidate({ node, offset, textOffset: start2 + offset, priority: 0 });
|
|
1370
|
+
}
|
|
1371
|
+
addCandidate({
|
|
1372
|
+
node,
|
|
1373
|
+
offset: node.data.length,
|
|
1374
|
+
textOffset: start2 + node.data.length,
|
|
1375
|
+
priority: 0
|
|
1376
|
+
});
|
|
1377
|
+
if (emergencyGraphemeBreaks) {
|
|
1378
|
+
for (let offset = 1; offset < node.data.length; offset++) {
|
|
1379
|
+
if (offset % 256 === 0) this.checkpoint();
|
|
1380
|
+
addCandidate({
|
|
1381
|
+
node,
|
|
1382
|
+
offset,
|
|
1383
|
+
textOffset: start2 + offset,
|
|
1384
|
+
priority: -1
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
754
1387
|
}
|
|
755
|
-
|
|
756
|
-
|
|
1388
|
+
}
|
|
1389
|
+
for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
|
|
1390
|
+
this.checkpoint();
|
|
1391
|
+
if (field.parentElement?.closest("[data-field]")) continue;
|
|
1392
|
+
const last = lastTextByField.get(field);
|
|
1393
|
+
const parent = field.parentNode;
|
|
1394
|
+
if (!last || !parent) continue;
|
|
1395
|
+
const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
|
|
1396
|
+
if (childIndex < 0) continue;
|
|
1397
|
+
addCandidate({
|
|
1398
|
+
node: parent,
|
|
1399
|
+
offset: childIndex + 1,
|
|
1400
|
+
textOffset: textStarts.get(last) + last.data.length,
|
|
1401
|
+
priority: 1
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
candidates.sort((left2, right2) => left2.textOffset - right2.textOffset || right2.priority - left2.priority);
|
|
1405
|
+
const endpoints = [];
|
|
1406
|
+
for (const candidate of candidates) {
|
|
1407
|
+
if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
|
|
1408
|
+
endpoints.push(candidate);
|
|
757
1409
|
}
|
|
758
1410
|
}
|
|
759
1411
|
return endpoints;
|
|
760
1412
|
}
|
|
1413
|
+
/** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
|
|
1414
|
+
graphemeBoundaryOffsets(text) {
|
|
1415
|
+
const SegmenterConstructor = this.view.Intl.Segmenter;
|
|
1416
|
+
if (!SegmenterConstructor) return null;
|
|
1417
|
+
const boundaries = /* @__PURE__ */ new Set([0, text.length]);
|
|
1418
|
+
let segmentCount = 0;
|
|
1419
|
+
for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
|
|
1420
|
+
if (++segmentCount % 256 === 0) this.checkpoint();
|
|
1421
|
+
boundaries.add(part.index);
|
|
1422
|
+
boundaries.add(part.index + part.segment.length);
|
|
1423
|
+
}
|
|
1424
|
+
return boundaries;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Conservative UAX #14/CSS wrapping opportunities used for synthetic page
|
|
1428
|
+
* boundaries. In particular, a formatting-run boundary is not itself a word
|
|
1429
|
+
* boundary, and Japanese opening/closing punctuation stays with its pair.
|
|
1430
|
+
* Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
|
|
1431
|
+
* the browser to wrap anywhere.
|
|
1432
|
+
*/
|
|
1433
|
+
isLegalParagraphLineBoundary(paragraph, text, offset) {
|
|
1434
|
+
const lastCodeUnit = text.charCodeAt(offset - 1);
|
|
1435
|
+
const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
|
|
1436
|
+
const beforeCodePoint = text.codePointAt(beforeOffset);
|
|
1437
|
+
const afterCodePoint = text.codePointAt(offset);
|
|
1438
|
+
const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
|
|
1439
|
+
const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
|
|
1440
|
+
if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
|
|
1441
|
+
if (/[-\u00AD\u2010]$/u.test(before)) return true;
|
|
1442
|
+
const style = this.view.getComputedStyle(paragraph);
|
|
1443
|
+
if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
|
|
1444
|
+
const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
1445
|
+
if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
|
|
1446
|
+
const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
|
|
1447
|
+
const closing = ")]}>\u2019\u201D\u3001\u3002\u3009\u300B\u300D\u300F\u3011\u3015\u3017\u3019\u301B\uFF01\uFF05\uFF09\uFF0C\uFF0E\uFF1A\uFF1B\uFF1F\uFF3D\uFF5D";
|
|
1448
|
+
return !opening.includes(before) && !closing.includes(after);
|
|
1449
|
+
}
|
|
1450
|
+
/** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
|
|
1451
|
+
isNonBreakingTextBoundary(text, offset) {
|
|
1452
|
+
const codePointBefore = (index) => {
|
|
1453
|
+
if (index <= 0) return void 0;
|
|
1454
|
+
const last = text.charCodeAt(index - 1);
|
|
1455
|
+
const start2 = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
|
|
1456
|
+
return text.codePointAt(Math.max(0, start2));
|
|
1457
|
+
};
|
|
1458
|
+
const nonBreaking = /* @__PURE__ */ new Set([
|
|
1459
|
+
160,
|
|
1460
|
+
8206,
|
|
1461
|
+
8207,
|
|
1462
|
+
8209,
|
|
1463
|
+
8234,
|
|
1464
|
+
8235,
|
|
1465
|
+
8236,
|
|
1466
|
+
8237,
|
|
1467
|
+
8238,
|
|
1468
|
+
8239,
|
|
1469
|
+
8288,
|
|
1470
|
+
8294,
|
|
1471
|
+
8295,
|
|
1472
|
+
8296,
|
|
1473
|
+
8297,
|
|
1474
|
+
65279
|
|
1475
|
+
]);
|
|
1476
|
+
return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Without Intl.Segmenter, admit only an all-ASCII boundary. This still
|
|
1480
|
+
* fragments ordinary prose while refusing to guess about Unicode clusters.
|
|
1481
|
+
*/
|
|
1482
|
+
isConservativeFallbackTextBoundary(text, offset) {
|
|
1483
|
+
return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
|
|
1484
|
+
}
|
|
761
1485
|
/**
|
|
762
1486
|
* Whether a range contains visible text after ignoring bidi/zero-width marks.
|
|
763
1487
|
* Paragraph fragmentation deliberately excludes non-textual descendants, so
|
|
764
1488
|
* this is enough to reject empty head or tail fragments.
|
|
765
1489
|
*/
|
|
766
1490
|
hasVisibleFragmentText(fragment) {
|
|
767
|
-
return (fragment.textContent || "")
|
|
1491
|
+
return this.hasVisibleText(fragment.textContent || "");
|
|
1492
|
+
}
|
|
1493
|
+
hasVisibleText(text) {
|
|
1494
|
+
return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
|
|
768
1495
|
}
|
|
769
1496
|
/**
|
|
770
1497
|
* Phase one only handles text-like paragraph descendants. Objects, explicit
|
|
@@ -773,13 +1500,25 @@ var DocxodusEditor = (() => {
|
|
|
773
1500
|
* fallback instead of risking broken content or duplicate anchors.
|
|
774
1501
|
*/
|
|
775
1502
|
canFragmentParagraph(block) {
|
|
776
|
-
if (!this.fragmentParagraphs
|
|
1503
|
+
if (!this.fragmentParagraphs) {
|
|
777
1504
|
return false;
|
|
778
1505
|
}
|
|
779
1506
|
const paragraph = block.element;
|
|
780
1507
|
if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
781
1508
|
return false;
|
|
782
1509
|
}
|
|
1510
|
+
return this.canRangeFragmentParagraph(paragraph);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Shared structural gate for body and note paragraph fragmentation. Footnote
|
|
1514
|
+
* first paragraphs are inline beside their marker, so that one known layout
|
|
1515
|
+
* context may opt into an inline root while retaining every descendant and
|
|
1516
|
+
* break-safety restriction used for body text.
|
|
1517
|
+
*/
|
|
1518
|
+
canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
|
|
1519
|
+
if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
783
1522
|
const unsupportedDescendants = [
|
|
784
1523
|
"br",
|
|
785
1524
|
"img",
|
|
@@ -810,21 +1549,27 @@ var DocxodusEditor = (() => {
|
|
|
810
1549
|
"fieldset",
|
|
811
1550
|
"details",
|
|
812
1551
|
"[data-footnote-id]",
|
|
813
|
-
"[data-list-marker]",
|
|
814
|
-
"[data-anchor]",
|
|
815
|
-
"[id]",
|
|
816
1552
|
"[contenteditable]"
|
|
817
1553
|
].join(", ");
|
|
818
1554
|
if (paragraph.querySelector(unsupportedDescendants)) {
|
|
819
1555
|
return false;
|
|
820
1556
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
1557
|
+
const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
|
|
1558
|
+
return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* A range clone preserves nested inline formatting exactly. Anything that establishes its own
|
|
1562
|
+
* box/layout context is deferred until a future fragmenter can model it accurately. Callers
|
|
1563
|
+
* must invoke this while the paragraph is attached to the styled document.
|
|
1564
|
+
*/
|
|
1565
|
+
hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
|
|
1566
|
+
const paragraphStyle = this.view.getComputedStyle(paragraph);
|
|
1567
|
+
if (paragraphStyle.display !== "block" && !(allowInlineRoot && paragraphStyle.display === "inline") || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" && paragraphStyle.whiteSpace !== "pre-wrap" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
|
|
823
1568
|
return false;
|
|
824
1569
|
}
|
|
825
1570
|
for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
|
|
826
|
-
const style =
|
|
827
|
-
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
|
|
1571
|
+
const style = this.view.getComputedStyle(descendant);
|
|
1572
|
+
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
|
|
828
1573
|
return false;
|
|
829
1574
|
}
|
|
830
1575
|
}
|
|
@@ -851,58 +1596,122 @@ var DocxodusEditor = (() => {
|
|
|
851
1596
|
return fragment;
|
|
852
1597
|
}
|
|
853
1598
|
/**
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
1599
|
+
* Range clones repeat an inline ancestor when the split lands inside it. Keep
|
|
1600
|
+
* semantic wrappers (links, comments, formatting) on both sides, but retain a
|
|
1601
|
+
* duplicated HTML/editor identity only on the leading fragment. Targets that
|
|
1602
|
+
* occur wholly after the split are absent from `head` and remain on `tail`.
|
|
857
1603
|
*/
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1604
|
+
reconcileParagraphFragmentIdentities(head, tail) {
|
|
1605
|
+
const elements = (root, selector2) => [
|
|
1606
|
+
...root.matches(selector2) ? [root] : [],
|
|
1607
|
+
...Array.from(root.querySelectorAll(selector2))
|
|
1608
|
+
];
|
|
1609
|
+
const headIds = new Set(elements(head, "[id]").map((element) => element.id));
|
|
1610
|
+
for (const element of elements(tail, "[id]")) {
|
|
1611
|
+
if (headIds.has(element.id)) element.removeAttribute("id");
|
|
861
1612
|
}
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
1613
|
+
const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
|
|
1614
|
+
for (const element of elements(tail, "[data-anchor]")) {
|
|
1615
|
+
if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
|
|
1616
|
+
element.removeAttribute("data-anchor");
|
|
1617
|
+
element.removeAttribute("data-committed-text");
|
|
866
1618
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Range-clone the largest safe prefix accepted by `fits`. The measurement
|
|
1622
|
+
* policy stays with the caller, allowing body blocks and note bands to share
|
|
1623
|
+
* one DOM fragmenter while measuring in their respective layout contexts.
|
|
1624
|
+
*/
|
|
1625
|
+
splitParagraphAtLargestFit(paragraph, fits, options = {}) {
|
|
1626
|
+
const largestFit = (endpoints2) => {
|
|
1627
|
+
const lastEndpoint = endpoints2[endpoints2.length - 1];
|
|
1628
|
+
if (lastEndpoint) {
|
|
1629
|
+
const lastRange = this.document.createRange();
|
|
1630
|
+
lastRange.setStart(paragraph, 0);
|
|
1631
|
+
lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
|
|
1632
|
+
const lastContents = lastRange.cloneContents();
|
|
1633
|
+
if (this.hasVisibleFragmentText(lastContents)) {
|
|
1634
|
+
const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
|
|
1635
|
+
if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
|
|
1636
|
+
}
|
|
880
1637
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1638
|
+
let low = 0;
|
|
1639
|
+
let high = endpoints2.length - 2;
|
|
1640
|
+
let best2 = null;
|
|
1641
|
+
while (low <= high) {
|
|
1642
|
+
this.checkpoint();
|
|
1643
|
+
const middle = Math.floor((low + high) / 2);
|
|
1644
|
+
const endpoint = endpoints2[middle];
|
|
1645
|
+
const headRange = this.document.createRange();
|
|
1646
|
+
headRange.setStart(paragraph, 0);
|
|
1647
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
1648
|
+
const headContents = headRange.cloneContents();
|
|
1649
|
+
if (!this.hasVisibleFragmentText(headContents)) {
|
|
1650
|
+
low = middle + 1;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const head = this.createParagraphFragment(paragraph, headRange, true, false);
|
|
1654
|
+
if (fits(head)) {
|
|
1655
|
+
best2 = { endpoint, head };
|
|
1656
|
+
low = middle + 1;
|
|
1657
|
+
} else {
|
|
1658
|
+
high = middle - 1;
|
|
1659
|
+
}
|
|
888
1660
|
}
|
|
1661
|
+
return best2;
|
|
1662
|
+
};
|
|
1663
|
+
let endpoints = this.paragraphFragmentEndpoints(paragraph);
|
|
1664
|
+
let best = largestFit(endpoints);
|
|
1665
|
+
if (!best && options.emergencyGraphemeBreaks) {
|
|
1666
|
+
endpoints = this.paragraphFragmentEndpoints(paragraph, true);
|
|
1667
|
+
best = largestFit(endpoints);
|
|
1668
|
+
}
|
|
1669
|
+
if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
|
|
1670
|
+
const endpoint = endpoints[0];
|
|
1671
|
+
const headRange = this.document.createRange();
|
|
1672
|
+
headRange.setStart(paragraph, 0);
|
|
1673
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
1674
|
+
best = {
|
|
1675
|
+
endpoint,
|
|
1676
|
+
head: this.createParagraphFragment(paragraph, headRange, true, false)
|
|
1677
|
+
};
|
|
889
1678
|
}
|
|
890
1679
|
if (!best) {
|
|
891
|
-
return
|
|
1680
|
+
return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
|
|
892
1681
|
}
|
|
893
|
-
const tailRange = document.createRange();
|
|
1682
|
+
const tailRange = this.document.createRange();
|
|
894
1683
|
tailRange.setStart(best.endpoint.node, best.endpoint.offset);
|
|
895
1684
|
tailRange.setEnd(paragraph, paragraph.childNodes.length);
|
|
896
1685
|
const tailContents = tailRange.cloneContents();
|
|
897
|
-
if (!this.hasVisibleFragmentText(tailContents)) {
|
|
1686
|
+
if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
|
|
1687
|
+
const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
|
|
1688
|
+
this.reconcileParagraphFragmentIdentities(best.head, tail);
|
|
1689
|
+
return {
|
|
1690
|
+
kind: "split",
|
|
1691
|
+
head: best.head,
|
|
1692
|
+
tail
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Splits a simple paragraph at the largest DOM Range endpoint that fits the
|
|
1697
|
+
* currently available body space. The caller then processes the tail normally,
|
|
1698
|
+
* allowing it to fragment again on later pages when necessary.
|
|
1699
|
+
*/
|
|
1700
|
+
tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
|
|
1701
|
+
if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
|
|
898
1702
|
return null;
|
|
899
1703
|
}
|
|
900
|
-
const
|
|
901
|
-
|
|
1704
|
+
const split = this.splitParagraphAtLargestFit(block.element, (head) => {
|
|
1705
|
+
const measured = this.measureElement(head, dims, block.sectionIndex);
|
|
1706
|
+
return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
|
|
1707
|
+
});
|
|
1708
|
+
if (split.kind !== "split") return null;
|
|
1709
|
+
const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
|
|
1710
|
+
const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
|
|
902
1711
|
return [
|
|
903
1712
|
{
|
|
904
|
-
...
|
|
905
|
-
element:
|
|
1713
|
+
...headMeasured,
|
|
1714
|
+
element: split.head,
|
|
906
1715
|
keepWithNext: false,
|
|
907
1716
|
keepLines: false,
|
|
908
1717
|
pageBreakBefore: false,
|
|
@@ -910,7 +1719,7 @@ var DocxodusEditor = (() => {
|
|
|
910
1719
|
},
|
|
911
1720
|
{
|
|
912
1721
|
...tailMeasured,
|
|
913
|
-
element: tail,
|
|
1722
|
+
element: split.tail,
|
|
914
1723
|
keepWithNext: false,
|
|
915
1724
|
keepLines: false,
|
|
916
1725
|
pageBreakBefore: false,
|
|
@@ -981,7 +1790,7 @@ var DocxodusEditor = (() => {
|
|
|
981
1790
|
const registry = /* @__PURE__ */ new Map();
|
|
982
1791
|
const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
|
|
983
1792
|
if (!registryEl) return registry;
|
|
984
|
-
const entries = Array.from(registryEl.
|
|
1793
|
+
const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
|
|
985
1794
|
for (const entry of entries) {
|
|
986
1795
|
const footnoteId = entry.dataset.footnoteId;
|
|
987
1796
|
if (footnoteId) {
|
|
@@ -990,6 +1799,68 @@ var DocxodusEditor = (() => {
|
|
|
990
1799
|
}
|
|
991
1800
|
return registry;
|
|
992
1801
|
}
|
|
1802
|
+
/** Read Word's optional normal and continuation separator stories. */
|
|
1803
|
+
parseFootnoteSeparators() {
|
|
1804
|
+
const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
|
|
1805
|
+
const clone = (kind) => {
|
|
1806
|
+
const source = Array.from(registry?.children ?? []).find(
|
|
1807
|
+
(entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
|
|
1808
|
+
);
|
|
1809
|
+
return source?.cloneNode(true);
|
|
1810
|
+
};
|
|
1811
|
+
return {
|
|
1812
|
+
normal: clone("normal") ?? null,
|
|
1813
|
+
continuation: clone("continuation") ?? null
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
/** Append the exact separator story selected for this initial/continued note band. */
|
|
1817
|
+
appendFootnoteSeparator(container, continuation) {
|
|
1818
|
+
const kind = continuation ? "continuation" : "normal";
|
|
1819
|
+
const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
|
|
1820
|
+
if (!source) {
|
|
1821
|
+
const fallback = this.document.createElement("hr");
|
|
1822
|
+
fallback.dataset.footnoteSeparator = kind;
|
|
1823
|
+
container.appendChild(fallback);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const clone = source.cloneNode(true);
|
|
1827
|
+
for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
|
|
1828
|
+
element.removeAttribute("id");
|
|
1829
|
+
element.removeAttribute("data-anchor");
|
|
1830
|
+
element.removeAttribute("data-committed-text");
|
|
1831
|
+
element.removeAttribute("data-source-anchor-id");
|
|
1832
|
+
element.removeAttribute("data-page-fragment-id");
|
|
1833
|
+
element.removeAttribute("data-fragment-index");
|
|
1834
|
+
element.removeAttribute("data-footnote-id");
|
|
1835
|
+
element.removeAttribute("data-comment-id");
|
|
1836
|
+
element.removeAttribute("name");
|
|
1837
|
+
if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
|
|
1838
|
+
element.removeAttribute("href");
|
|
1839
|
+
}
|
|
1840
|
+
element.setAttribute("contenteditable", "false");
|
|
1841
|
+
}
|
|
1842
|
+
container.appendChild(clone);
|
|
1843
|
+
}
|
|
1844
|
+
/** Parses the hidden source notes used to render paginated margin comments. */
|
|
1845
|
+
parseCommentMarginRegistry() {
|
|
1846
|
+
const registry = /* @__PURE__ */ new Map();
|
|
1847
|
+
const registryEl = this.stagingElement.querySelector(
|
|
1848
|
+
"#pagination-comment-margin-registry"
|
|
1849
|
+
);
|
|
1850
|
+
if (!registryEl) return registry;
|
|
1851
|
+
for (const entry of Array.from(
|
|
1852
|
+
registryEl.querySelectorAll("[data-comment-id]")
|
|
1853
|
+
)) {
|
|
1854
|
+
const commentId = entry.dataset.commentId;
|
|
1855
|
+
if (!commentId) continue;
|
|
1856
|
+
let root = entry;
|
|
1857
|
+
for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
|
|
1858
|
+
root = parent;
|
|
1859
|
+
}
|
|
1860
|
+
registry.set(commentId, root.cloneNode(true));
|
|
1861
|
+
}
|
|
1862
|
+
return registry;
|
|
1863
|
+
}
|
|
993
1864
|
/**
|
|
994
1865
|
* Extracts footnote reference IDs from an element.
|
|
995
1866
|
*/
|
|
@@ -1004,6 +1875,92 @@ var DocxodusEditor = (() => {
|
|
|
1004
1875
|
}
|
|
1005
1876
|
return ids;
|
|
1006
1877
|
}
|
|
1878
|
+
/** Clone a note child for the continuation queue with its original selector position. */
|
|
1879
|
+
cloneFootnoteElementForContinuation(element, sourceIndex) {
|
|
1880
|
+
const clone = element.cloneNode(true);
|
|
1881
|
+
clone.setAttribute(
|
|
1882
|
+
FOOTNOTE_SOURCE_POSITION_ATTR,
|
|
1883
|
+
sourceIndex === 0 ? "first" : "later"
|
|
1884
|
+
);
|
|
1885
|
+
return clone;
|
|
1886
|
+
}
|
|
1887
|
+
/** Remove identities that belong only to the initial registry presentation shell. */
|
|
1888
|
+
makeContinuationShellInert(element) {
|
|
1889
|
+
element.removeAttribute("id");
|
|
1890
|
+
element.removeAttribute("data-anchor");
|
|
1891
|
+
element.removeAttribute("data-committed-text");
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Build the exact continuation shape shared by measurement and paint.
|
|
1895
|
+
*
|
|
1896
|
+
* Keep the source `.footnote-item > .footnote-content` ancestry: document
|
|
1897
|
+
* CSS, inherited direction/language, and custom classes frequently target
|
|
1898
|
+
* those shells. Only the number and initial HTML/editor identities are
|
|
1899
|
+
* omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
|
|
1900
|
+
* whose first carried element was a later source paragraph.
|
|
1901
|
+
*/
|
|
1902
|
+
createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
|
|
1903
|
+
const source = this.footnoteRegistry.get(continuation.footnoteId);
|
|
1904
|
+
const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
|
|
1905
|
+
this.makeContinuationShellInert(wrapper);
|
|
1906
|
+
wrapper.classList.add("footnote-continuation");
|
|
1907
|
+
wrapper.dataset.footnoteId = continuation.footnoteId;
|
|
1908
|
+
if (continuation.sourceAnchorId) {
|
|
1909
|
+
wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
|
|
1910
|
+
}
|
|
1911
|
+
const sourceContent = source?.querySelector(".footnote-content");
|
|
1912
|
+
const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
|
|
1913
|
+
this.makeContinuationShellInert(content);
|
|
1914
|
+
if (!sourceContent) content.className = "footnote-content";
|
|
1915
|
+
const first = elements[0];
|
|
1916
|
+
if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
|
|
1917
|
+
const sentinel = this.document.createElement("p");
|
|
1918
|
+
sentinel.className = "footnote-continuation-position-sentinel";
|
|
1919
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
1920
|
+
sentinel.style.setProperty("display", "none", "important");
|
|
1921
|
+
content.appendChild(sentinel);
|
|
1922
|
+
}
|
|
1923
|
+
for (let index = 0; index < elements.length; index++) {
|
|
1924
|
+
this.checkpoint();
|
|
1925
|
+
const element = elements[index];
|
|
1926
|
+
const clone = element.cloneNode(true);
|
|
1927
|
+
clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
1928
|
+
if (index === 0 && clone.tagName === "P") {
|
|
1929
|
+
clone.style.setProperty("display", "block", "important");
|
|
1930
|
+
}
|
|
1931
|
+
content.appendChild(clone);
|
|
1932
|
+
}
|
|
1933
|
+
wrapper.appendChild(content);
|
|
1934
|
+
return wrapper;
|
|
1935
|
+
}
|
|
1936
|
+
/** Build the exact partial-note item shape shared by measurement and paint. */
|
|
1937
|
+
createPartialFootnoteItem(footnote, fittingElements) {
|
|
1938
|
+
const item = footnote.cloneNode(false);
|
|
1939
|
+
const number = footnote.querySelector(".footnote-number");
|
|
1940
|
+
if (number) item.appendChild(number.cloneNode(true));
|
|
1941
|
+
const sourceContent = footnote.querySelector(".footnote-content");
|
|
1942
|
+
const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
|
|
1943
|
+
if (!sourceContent) content.className = "footnote-content";
|
|
1944
|
+
for (const element of fittingElements) {
|
|
1945
|
+
this.checkpoint();
|
|
1946
|
+
content.appendChild(element.cloneNode(true));
|
|
1947
|
+
}
|
|
1948
|
+
item.appendChild(content);
|
|
1949
|
+
return item;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Where a note measurement tree is attached.
|
|
1953
|
+
*
|
|
1954
|
+
* A note band is reserved from a measurement and then painted; if the two
|
|
1955
|
+
* happen under different inherited typography the painted notes overflow the
|
|
1956
|
+
* reserve, which is the invisible clipping this engine exists to avoid. The
|
|
1957
|
+
* registry lives in the staging tree but a band paints inside the output
|
|
1958
|
+
* container, so measure there. The host is never a page box, so a page's
|
|
1959
|
+
* `zoom` cannot scale a reserve that is accounted for in unscaled points.
|
|
1960
|
+
*/
|
|
1961
|
+
noteMeasurementHost() {
|
|
1962
|
+
return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
|
|
1963
|
+
}
|
|
1007
1964
|
/**
|
|
1008
1965
|
* Measures the height of footnotes for given IDs (in points).
|
|
1009
1966
|
* Creates a temporary container to measure the footnotes.
|
|
@@ -1011,140 +1968,246 @@ var DocxodusEditor = (() => {
|
|
|
1011
1968
|
* @param contentWidth - Width for measurement
|
|
1012
1969
|
* @param continuation - Optional continuation content to include first
|
|
1013
1970
|
*/
|
|
1014
|
-
measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
|
|
1971
|
+
measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
|
|
1015
1972
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
1016
|
-
if (footnoteIds.length === 0 && !hasContinuation
|
|
1973
|
+
if (footnoteIds.length === 0 && !hasContinuation) {
|
|
1017
1974
|
return 0;
|
|
1018
1975
|
}
|
|
1019
|
-
|
|
1976
|
+
if (this.footnoteRegistry.size === 0 && !hasContinuation) {
|
|
1977
|
+
return 0;
|
|
1978
|
+
}
|
|
1979
|
+
const measureContainer = this.document.createElement("div");
|
|
1020
1980
|
measureContainer.style.position = "absolute";
|
|
1021
1981
|
measureContainer.style.visibility = "hidden";
|
|
1022
1982
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1023
1983
|
measureContainer.style.left = "-9999px";
|
|
1024
1984
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
1025
|
-
|
|
1026
|
-
measureContainer.appendChild(hr);
|
|
1985
|
+
this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
|
|
1027
1986
|
if (hasContinuation) {
|
|
1028
|
-
|
|
1029
|
-
contWrapper.className = "footnote-continuation";
|
|
1030
|
-
for (const el of continuation.remainingElements) {
|
|
1031
|
-
contWrapper.appendChild(el.cloneNode(true));
|
|
1032
|
-
}
|
|
1033
|
-
measureContainer.appendChild(contWrapper);
|
|
1987
|
+
measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
|
|
1034
1988
|
}
|
|
1989
|
+
const partialById = new Map(
|
|
1990
|
+
partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
|
|
1991
|
+
);
|
|
1035
1992
|
for (const id of footnoteIds) {
|
|
1993
|
+
this.checkpoint();
|
|
1036
1994
|
const footnote = this.footnoteRegistry.get(id);
|
|
1037
1995
|
if (footnote) {
|
|
1038
|
-
|
|
1996
|
+
const partial = partialById.get(id);
|
|
1997
|
+
measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
|
|
1039
1998
|
}
|
|
1040
1999
|
}
|
|
1041
|
-
this.
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
2000
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2001
|
+
try {
|
|
2002
|
+
return pxToPt(measureContainer.getBoundingClientRect().height);
|
|
2003
|
+
} finally {
|
|
2004
|
+
measureContainer.remove();
|
|
2005
|
+
}
|
|
1046
2006
|
}
|
|
1047
2007
|
/**
|
|
1048
|
-
*
|
|
2008
|
+
* Partition a continuation for one page's note band, preferring complete children
|
|
2009
|
+
* and range-fragmenting an eligible paragraph when necessary. Always advances by
|
|
2010
|
+
* at least one element so an indivisible oversized paragraph follows the established
|
|
2011
|
+
* clipped fallback without trapping pagination in a loop.
|
|
1049
2012
|
*/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const measureContainer = document.createElement("div");
|
|
2013
|
+
splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
|
|
2014
|
+
const fitting = [];
|
|
2015
|
+
let remaining = [];
|
|
2016
|
+
const measureContainer = this.document.createElement("div");
|
|
1055
2017
|
measureContainer.style.position = "absolute";
|
|
1056
2018
|
measureContainer.style.visibility = "hidden";
|
|
1057
2019
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1058
2020
|
measureContainer.style.left = "-9999px";
|
|
1059
2021
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
2022
|
+
this.appendFootnoteSeparator(measureContainer, true);
|
|
2023
|
+
const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
|
|
2024
|
+
const content = wrapper.querySelector(":scope > .footnote-content");
|
|
2025
|
+
if (!content) {
|
|
2026
|
+
throw new Error("Footnote continuation is missing its content shell");
|
|
2027
|
+
}
|
|
2028
|
+
measureContainer.appendChild(wrapper);
|
|
2029
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2030
|
+
try {
|
|
2031
|
+
for (let index = 0; index < continuation.remainingElements.length; index++) {
|
|
2032
|
+
this.checkpoint();
|
|
2033
|
+
const element = continuation.remainingElements[index];
|
|
2034
|
+
const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
2035
|
+
if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
|
|
2036
|
+
const sentinel = this.document.createElement("p");
|
|
2037
|
+
sentinel.className = "footnote-continuation-position-sentinel";
|
|
2038
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
2039
|
+
sentinel.style.setProperty("display", "none", "important");
|
|
2040
|
+
content.appendChild(sentinel);
|
|
2041
|
+
}
|
|
2042
|
+
const candidate = element.cloneNode(true);
|
|
2043
|
+
candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
2044
|
+
if (fitting.length === 0 && candidate.tagName === "P") {
|
|
2045
|
+
candidate.style.setProperty("display", "block", "important");
|
|
2046
|
+
}
|
|
2047
|
+
content.appendChild(candidate);
|
|
2048
|
+
const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
|
|
2049
|
+
if (candidateHeight <= availableHeightPt) {
|
|
2050
|
+
fitting.push(element);
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
|
|
2054
|
+
candidate.remove();
|
|
2055
|
+
let split = null;
|
|
2056
|
+
if (canSplitCandidate) {
|
|
2057
|
+
split = this.splitParagraphAtLargestFit(candidate, (head) => {
|
|
2058
|
+
content.appendChild(head);
|
|
2059
|
+
try {
|
|
2060
|
+
return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2061
|
+
} finally {
|
|
2062
|
+
head.remove();
|
|
2063
|
+
}
|
|
2064
|
+
}, {
|
|
2065
|
+
// Both fallbacks below cut where ordinary line breaking would not,
|
|
2066
|
+
// so they are only ever right for an element that owns the whole
|
|
2067
|
+
// band: if this page already carries earlier content, a paragraph
|
|
2068
|
+
// that will not start here belongs intact in the next note band.
|
|
2069
|
+
emergencyGraphemeBreaks: fitting.length === 0,
|
|
2070
|
+
// A continuation page owns the full note band. If even one legal
|
|
2071
|
+
// grapheme is taller than it, clip only that unit and keep draining.
|
|
2072
|
+
forceFirstOnNoFit: fitting.length === 0
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
if (split?.kind === "split") {
|
|
2076
|
+
if (sourcePosition) {
|
|
2077
|
+
split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
|
|
2078
|
+
split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
|
|
2079
|
+
}
|
|
2080
|
+
fitting.push(split.head);
|
|
2081
|
+
remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
|
|
2082
|
+
} else if (fitting.length === 0) {
|
|
2083
|
+
fitting.push(element);
|
|
2084
|
+
remaining = continuation.remainingElements.slice(index + 1);
|
|
2085
|
+
} else {
|
|
2086
|
+
remaining = continuation.remainingElements.slice(index);
|
|
1124
2087
|
}
|
|
1125
2088
|
break;
|
|
1126
2089
|
}
|
|
2090
|
+
} finally {
|
|
2091
|
+
measureContainer.remove();
|
|
1127
2092
|
}
|
|
1128
|
-
return {
|
|
2093
|
+
return {
|
|
2094
|
+
current: {
|
|
2095
|
+
footnoteId: continuation.footnoteId,
|
|
2096
|
+
sourceAnchorId: continuation.sourceAnchorId,
|
|
2097
|
+
remainingElements: fitting
|
|
2098
|
+
},
|
|
2099
|
+
overflow: remaining.length > 0 ? {
|
|
2100
|
+
footnoteId: continuation.footnoteId,
|
|
2101
|
+
sourceAnchorId: continuation.sourceAnchorId,
|
|
2102
|
+
remainingElements: remaining
|
|
2103
|
+
} : null
|
|
2104
|
+
};
|
|
1129
2105
|
}
|
|
1130
2106
|
/**
|
|
1131
|
-
*
|
|
2107
|
+
* Splits a footnote element into parts that fit within the available height.
|
|
2108
|
+
* Returns the elements that fit and the elements that need to continue.
|
|
1132
2109
|
*/
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
2110
|
+
splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
|
|
2111
|
+
const layoutContext = this.document.createElement("div");
|
|
2112
|
+
layoutContext.style.position = "absolute";
|
|
2113
|
+
layoutContext.style.visibility = "hidden";
|
|
2114
|
+
layoutContext.style.width = `${contentWidth}pt`;
|
|
2115
|
+
layoutContext.style.left = "-9999px";
|
|
2116
|
+
layoutContext.className = this.cssPrefix + "footnotes";
|
|
2117
|
+
this.appendFootnoteSeparator(layoutContext, false);
|
|
2118
|
+
const attachedFootnote = footnoteElement.cloneNode(true);
|
|
2119
|
+
layoutContext.appendChild(attachedFootnote);
|
|
2120
|
+
this.noteMeasurementHost().appendChild(layoutContext);
|
|
2121
|
+
const measureContainer = this.document.createElement("div");
|
|
1137
2122
|
measureContainer.style.position = "absolute";
|
|
1138
2123
|
measureContainer.style.visibility = "hidden";
|
|
1139
2124
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1140
2125
|
measureContainer.style.left = "-9999px";
|
|
1141
2126
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
1142
|
-
|
|
1143
|
-
this.
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2127
|
+
const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
|
|
2128
|
+
this.appendFootnoteSeparator(measureContainer, hasContinuation);
|
|
2129
|
+
if (hasContinuation) {
|
|
2130
|
+
measureContainer.appendChild(this.createFootnoteContinuationWrapper(
|
|
2131
|
+
existingPayload.continuation
|
|
2132
|
+
));
|
|
2133
|
+
}
|
|
2134
|
+
const partialById = new Map(
|
|
2135
|
+
existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
|
|
2136
|
+
);
|
|
2137
|
+
for (const id of existingPayload?.footnoteIds ?? []) {
|
|
2138
|
+
this.checkpoint();
|
|
2139
|
+
const source = this.footnoteRegistry.get(id);
|
|
2140
|
+
if (!source) continue;
|
|
2141
|
+
const partial = partialById.get(id);
|
|
2142
|
+
measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
|
|
2143
|
+
}
|
|
2144
|
+
const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
|
|
2145
|
+
const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
|
|
2146
|
+
if (!measuredContent) {
|
|
2147
|
+
layoutContext.remove();
|
|
2148
|
+
throw new Error("Footnote is missing its content shell");
|
|
2149
|
+
}
|
|
2150
|
+
measureContainer.appendChild(measuredPartial);
|
|
2151
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2152
|
+
try {
|
|
2153
|
+
const footnoteContent = attachedFootnote.querySelector(".footnote-content");
|
|
2154
|
+
if (!footnoteContent) {
|
|
2155
|
+
return {
|
|
2156
|
+
fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
|
|
2157
|
+
overflow: []
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
const children = Array.from(footnoteContent.children);
|
|
2161
|
+
const fits = [];
|
|
2162
|
+
let overflow = [];
|
|
2163
|
+
for (let i = 0; i < children.length; i++) {
|
|
2164
|
+
this.checkpoint();
|
|
2165
|
+
const child = children[i];
|
|
2166
|
+
const measuredCandidate = child.cloneNode(true);
|
|
2167
|
+
measuredContent.appendChild(measuredCandidate);
|
|
2168
|
+
const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2169
|
+
if (candidateFits) {
|
|
2170
|
+
fits.push(child.cloneNode(true));
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
measuredCandidate.remove();
|
|
2174
|
+
const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
|
|
2175
|
+
measuredContent.appendChild(head);
|
|
2176
|
+
try {
|
|
2177
|
+
return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2178
|
+
} finally {
|
|
2179
|
+
head.remove();
|
|
2180
|
+
}
|
|
2181
|
+
}, {
|
|
2182
|
+
// Both fallbacks cut where ordinary line breaking would not, so they
|
|
2183
|
+
// are only ever right for an element that owns the whole band: with
|
|
2184
|
+
// earlier siblings already packed here, a paragraph that will not
|
|
2185
|
+
// start belongs intact in the next note band.
|
|
2186
|
+
emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
|
|
2187
|
+
forceFirstOnNoFit: fits.length === 0 && forceProgress
|
|
2188
|
+
}) : null;
|
|
2189
|
+
if (split?.kind === "split") {
|
|
2190
|
+
fits.push(split.head);
|
|
2191
|
+
split.tail.setAttribute(
|
|
2192
|
+
FOOTNOTE_SOURCE_POSITION_ATTR,
|
|
2193
|
+
i === 0 ? "first" : "later"
|
|
2194
|
+
);
|
|
2195
|
+
overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
|
|
2196
|
+
} else if (split?.kind === "no-fit") {
|
|
2197
|
+
overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
|
|
2198
|
+
} else if (fits.length === 0 && forceProgress) {
|
|
2199
|
+
fits.push(child.cloneNode(true));
|
|
2200
|
+
overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
|
|
2201
|
+
} else {
|
|
2202
|
+
overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
|
|
2203
|
+
}
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
return { fits, overflow };
|
|
2207
|
+
} finally {
|
|
2208
|
+
measureContainer.remove();
|
|
2209
|
+
layoutContext.remove();
|
|
2210
|
+
}
|
|
1148
2211
|
}
|
|
1149
2212
|
/**
|
|
1150
2213
|
* Adds footnotes to a page container, including continuation content.
|
|
@@ -1157,12 +2220,11 @@ var DocxodusEditor = (() => {
|
|
|
1157
2220
|
if (this.footnoteRegistry.size === 0 && !hasContinuation) {
|
|
1158
2221
|
return;
|
|
1159
2222
|
}
|
|
1160
|
-
const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
|
|
1161
2223
|
const maxFootnoteHeight = Math.min(
|
|
1162
2224
|
footnoteHeight,
|
|
1163
2225
|
bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
|
|
1164
2226
|
);
|
|
1165
|
-
const footnotesDiv = document.createElement("div");
|
|
2227
|
+
const footnotesDiv = this.document.createElement("div");
|
|
1166
2228
|
footnotesDiv.className = `${this.cssPrefix}footnotes`;
|
|
1167
2229
|
footnotesDiv.style.position = "absolute";
|
|
1168
2230
|
footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
|
|
@@ -1171,35 +2233,18 @@ var DocxodusEditor = (() => {
|
|
|
1171
2233
|
footnotesDiv.style.boxSizing = "border-box";
|
|
1172
2234
|
footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
|
|
1173
2235
|
footnotesDiv.style.overflow = "hidden";
|
|
1174
|
-
|
|
1175
|
-
footnotesDiv.appendChild(hr);
|
|
2236
|
+
this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
|
|
1176
2237
|
if (hasContinuation) {
|
|
1177
|
-
|
|
1178
|
-
contWrapper.className = "footnote-continuation";
|
|
1179
|
-
for (const el of continuation.remainingElements) {
|
|
1180
|
-
contWrapper.appendChild(el.cloneNode(true));
|
|
1181
|
-
}
|
|
1182
|
-
footnotesDiv.appendChild(contWrapper);
|
|
2238
|
+
footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
|
|
1183
2239
|
}
|
|
1184
2240
|
for (const id of footnoteIds) {
|
|
1185
2241
|
const partial = partialFootnotes?.find((p) => p.footnoteId === id);
|
|
1186
2242
|
if (partial) {
|
|
1187
2243
|
const footnote = this.footnoteRegistry.get(id);
|
|
1188
2244
|
if (footnote) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const numberSpan = footnote.querySelector(".footnote-number");
|
|
1193
|
-
if (numberSpan) {
|
|
1194
|
-
partialDiv.appendChild(numberSpan.cloneNode(true));
|
|
1195
|
-
}
|
|
1196
|
-
const contentSpan = document.createElement("span");
|
|
1197
|
-
contentSpan.className = "footnote-content";
|
|
1198
|
-
for (const el of partial.fittingElements) {
|
|
1199
|
-
contentSpan.appendChild(el.cloneNode(true));
|
|
1200
|
-
}
|
|
1201
|
-
partialDiv.appendChild(contentSpan);
|
|
1202
|
-
footnotesDiv.appendChild(partialDiv);
|
|
2245
|
+
footnotesDiv.appendChild(
|
|
2246
|
+
this.createPartialFootnoteItem(footnote, partial.fittingElements)
|
|
2247
|
+
);
|
|
1203
2248
|
}
|
|
1204
2249
|
} else {
|
|
1205
2250
|
const footnote = this.footnoteRegistry.get(id);
|
|
@@ -1210,30 +2255,26 @@ var DocxodusEditor = (() => {
|
|
|
1210
2255
|
}
|
|
1211
2256
|
pageBox.appendChild(footnotesDiv);
|
|
1212
2257
|
}
|
|
1213
|
-
/**
|
|
1214
|
-
|
|
1215
|
-
*/
|
|
1216
|
-
selectHeader(sectionIndex, pageInSection, globalPageNumber) {
|
|
2258
|
+
/** Select the section's first/odd/even header from its one-based page position. */
|
|
2259
|
+
selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
|
|
1217
2260
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1218
2261
|
if (!sectionHf) return void 0;
|
|
1219
2262
|
if (pageInSection === 1 && sectionHf.headerFirst) {
|
|
1220
2263
|
return sectionHf.headerFirst;
|
|
1221
2264
|
}
|
|
1222
|
-
if (
|
|
2265
|
+
if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
|
|
1223
2266
|
return sectionHf.headerEven;
|
|
1224
2267
|
}
|
|
1225
2268
|
return sectionHf.headerDefault;
|
|
1226
2269
|
}
|
|
1227
|
-
/**
|
|
1228
|
-
|
|
1229
|
-
*/
|
|
1230
|
-
selectFooter(sectionIndex, pageInSection, globalPageNumber) {
|
|
2270
|
+
/** Select the section's first/odd/even footer from its one-based page position. */
|
|
2271
|
+
selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
|
|
1231
2272
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1232
2273
|
if (!sectionHf) return void 0;
|
|
1233
2274
|
if (pageInSection === 1 && sectionHf.footerFirst) {
|
|
1234
2275
|
return sectionHf.footerFirst;
|
|
1235
2276
|
}
|
|
1236
|
-
if (
|
|
2277
|
+
if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
|
|
1237
2278
|
return sectionHf.footerEven;
|
|
1238
2279
|
}
|
|
1239
2280
|
return sectionHf.footerDefault;
|
|
@@ -1248,7 +2289,7 @@ var DocxodusEditor = (() => {
|
|
|
1248
2289
|
* Deterministic: it depends only on the section's page setup and the registry's pre-measured
|
|
1249
2290
|
* story heights, never on the page's content, which is what keeps it lazy-loading compatible.
|
|
1250
2291
|
*/
|
|
1251
|
-
getPageBands(dims, sectionIndex, pageInSection,
|
|
2292
|
+
getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
|
|
1252
2293
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1253
2294
|
return resolvePageBands(
|
|
1254
2295
|
dims,
|
|
@@ -1257,14 +2298,14 @@ var DocxodusEditor = (() => {
|
|
|
1257
2298
|
sectionHf?.headerEvenHeight,
|
|
1258
2299
|
sectionHf?.headerDefaultHeight,
|
|
1259
2300
|
pageInSection,
|
|
1260
|
-
|
|
2301
|
+
displayedPageNumber
|
|
1261
2302
|
),
|
|
1262
2303
|
this.selectStoryHeight(
|
|
1263
2304
|
sectionHf?.footerFirstHeight,
|
|
1264
2305
|
sectionHf?.footerEvenHeight,
|
|
1265
2306
|
sectionHf?.footerDefaultHeight,
|
|
1266
2307
|
pageInSection,
|
|
1267
|
-
|
|
2308
|
+
displayedPageNumber
|
|
1268
2309
|
)
|
|
1269
2310
|
);
|
|
1270
2311
|
}
|
|
@@ -1272,9 +2313,9 @@ var DocxodusEditor = (() => {
|
|
|
1272
2313
|
* The measured height of the running story this page position selects, mirroring
|
|
1273
2314
|
* {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
|
|
1274
2315
|
*/
|
|
1275
|
-
selectStoryHeight(first, even, fallback, pageInSection,
|
|
2316
|
+
selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
|
|
1276
2317
|
if (pageInSection === 1 && first != null) return first;
|
|
1277
|
-
if (
|
|
2318
|
+
if (displayedPageNumber % 2 === 0 && even != null) return even;
|
|
1278
2319
|
return fallback ?? 0;
|
|
1279
2320
|
}
|
|
1280
2321
|
/**
|
|
@@ -1286,7 +2327,7 @@ var DocxodusEditor = (() => {
|
|
|
1286
2327
|
* silently starts overlapping body text.
|
|
1287
2328
|
*/
|
|
1288
2329
|
measureHeaderFooterHeight(source, contentWidth) {
|
|
1289
|
-
const measureContainer = document.createElement("div");
|
|
2330
|
+
const measureContainer = this.document.createElement("div");
|
|
1290
2331
|
measureContainer.style.position = "absolute";
|
|
1291
2332
|
measureContainer.style.visibility = "hidden";
|
|
1292
2333
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
@@ -1305,79 +2346,275 @@ var DocxodusEditor = (() => {
|
|
|
1305
2346
|
* Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
|
|
1306
2347
|
* Supports footnote continuation - long footnotes can split across pages.
|
|
1307
2348
|
*/
|
|
1308
|
-
flowToPages(blocks, dims, startPageNumber, sectionIndex) {
|
|
2349
|
+
flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
|
|
1309
2350
|
const pages = [];
|
|
1310
2351
|
let currentContent = [];
|
|
1311
2352
|
let pageNumber = startPageNumber;
|
|
1312
|
-
let
|
|
2353
|
+
let pageSectionIndex = sectionIndex;
|
|
2354
|
+
const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
|
|
2355
|
+
const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
|
|
2356
|
+
const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
|
|
2357
|
+
const markSectionPlaced = (owner) => {
|
|
2358
|
+
if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
|
|
2359
|
+
};
|
|
2360
|
+
const previousBox = this.containerElement.querySelector(
|
|
2361
|
+
`.${this.cssPrefix}box:last-of-type`
|
|
2362
|
+
);
|
|
2363
|
+
let precedingDisplayedPageNumber = parseInt(
|
|
2364
|
+
previousBox?.dataset.displayedPageNumber ?? "0",
|
|
2365
|
+
10
|
|
2366
|
+
);
|
|
2367
|
+
const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
|
|
2368
|
+
const numbering = this.pageNumbering.get(owner) ?? {};
|
|
2369
|
+
if (numbering.start !== void 0) {
|
|
2370
|
+
return numbering.start + ownerPageInSection - 1;
|
|
2371
|
+
}
|
|
2372
|
+
const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
|
|
2373
|
+
const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
|
|
2374
|
+
return currentPhysicalNumber + physicalPage - pageNumber;
|
|
2375
|
+
};
|
|
1313
2376
|
let { bodyHeight: effectiveContentHeight } = this.getPageBands(
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
pageInSection,
|
|
1317
|
-
|
|
2377
|
+
dimensionsFor(),
|
|
2378
|
+
pageSectionIndex,
|
|
2379
|
+
pageInSection(),
|
|
2380
|
+
displayedPageNumber()
|
|
1318
2381
|
);
|
|
1319
2382
|
let remainingHeight = effectiveContentHeight;
|
|
1320
2383
|
let prevMarginBottomPt = 0;
|
|
1321
2384
|
let currentFootnoteIds = [];
|
|
2385
|
+
let currentPageHasFootnoteReference = false;
|
|
1322
2386
|
let currentFootnoteHeight = 0;
|
|
1323
2387
|
let currentContinuation = this.pendingFootnoteContinuation;
|
|
1324
2388
|
let nextPageContinuation = null;
|
|
2389
|
+
let currentContinuationPartitioned = false;
|
|
2390
|
+
let currentPageAdmitted = false;
|
|
1325
2391
|
let deferredFootnoteIds = [];
|
|
1326
2392
|
let currentPartialFootnotes = [];
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2393
|
+
const adoptEmptyPageOwner = (owner) => {
|
|
2394
|
+
pageSectionIndex = owner;
|
|
2395
|
+
const bands = this.getPageBands(
|
|
2396
|
+
dimensionsFor(owner),
|
|
2397
|
+
owner,
|
|
2398
|
+
pageInSection(owner),
|
|
2399
|
+
displayedPageNumber(owner)
|
|
2400
|
+
);
|
|
2401
|
+
effectiveContentHeight = bands.bodyHeight;
|
|
2402
|
+
remainingHeight = effectiveContentHeight;
|
|
2403
|
+
};
|
|
2404
|
+
const admitCurrentPage = () => {
|
|
2405
|
+
if (currentPageAdmitted) return;
|
|
2406
|
+
this.admitPageAllocation();
|
|
2407
|
+
currentPageAdmitted = true;
|
|
2408
|
+
};
|
|
2409
|
+
const prepareCurrentContinuation = () => {
|
|
2410
|
+
if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
|
|
2411
|
+
admitCurrentPage();
|
|
2412
|
+
const ownedDimensions = dimensionsFor();
|
|
2413
|
+
const pageBands = this.getPageBands(
|
|
2414
|
+
ownedDimensions,
|
|
2415
|
+
pageSectionIndex,
|
|
2416
|
+
pageInSection(),
|
|
2417
|
+
displayedPageNumber()
|
|
2418
|
+
);
|
|
2419
|
+
const partition = this.splitContinuationForPage(
|
|
2420
|
+
currentContinuation,
|
|
2421
|
+
pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
|
|
2422
|
+
ownedDimensions.contentWidth
|
|
2423
|
+
);
|
|
2424
|
+
currentContinuation = partition.current;
|
|
2425
|
+
if (partition.overflow) nextPageContinuation = partition.overflow;
|
|
2426
|
+
currentContinuationPartitioned = true;
|
|
2427
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2428
|
+
currentFootnoteIds,
|
|
2429
|
+
ownedDimensions.contentWidth,
|
|
2430
|
+
currentContinuation,
|
|
2431
|
+
currentPartialFootnotes
|
|
2432
|
+
);
|
|
2433
|
+
};
|
|
2434
|
+
const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
|
|
1331
2435
|
const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1332
|
-
if (currentContent.length === 0 && !hasCurrentContinuation)
|
|
2436
|
+
if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
|
|
2437
|
+
adoptEmptyPageOwner(nextPageSectionIndex);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
prepareCurrentContinuation();
|
|
2441
|
+
admitCurrentPage();
|
|
2442
|
+
let pageContinuation = currentContinuation;
|
|
2443
|
+
const ownedPageInSection = pageInSection();
|
|
2444
|
+
const ownedDimensions = dimensionsFor();
|
|
2445
|
+
const ownedDisplayedPageNumber = displayedPageNumber(
|
|
2446
|
+
pageSectionIndex,
|
|
2447
|
+
ownedPageInSection
|
|
2448
|
+
);
|
|
2449
|
+
const pageBands = this.getPageBands(
|
|
2450
|
+
ownedDimensions,
|
|
2451
|
+
pageSectionIndex,
|
|
2452
|
+
ownedPageInSection,
|
|
2453
|
+
ownedDisplayedPageNumber
|
|
2454
|
+
);
|
|
2455
|
+
const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2456
|
+
pageContinuation = currentContinuation;
|
|
2457
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2458
|
+
currentFootnoteIds,
|
|
2459
|
+
ownedDimensions.contentWidth,
|
|
2460
|
+
pageContinuation,
|
|
2461
|
+
currentPartialFootnotes
|
|
2462
|
+
);
|
|
2463
|
+
if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
|
|
2464
|
+
const fittingIds = [];
|
|
2465
|
+
for (let index = 0; index < currentFootnoteIds.length; index++) {
|
|
2466
|
+
const footnoteId = currentFootnoteIds[index];
|
|
2467
|
+
const candidateIds = [...fittingIds, footnoteId];
|
|
2468
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2469
|
+
candidateIds,
|
|
2470
|
+
ownedDimensions.contentWidth,
|
|
2471
|
+
pageContinuation
|
|
2472
|
+
);
|
|
2473
|
+
const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
|
|
2474
|
+
if (guardedCandidateHeight <= maxFootnoteHeight) {
|
|
2475
|
+
fittingIds.push(footnoteId);
|
|
2476
|
+
currentFootnoteHeight = guardedCandidateHeight;
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
|
|
2480
|
+
if (fittingIds.length === 0 && !hasPageContinuation) {
|
|
2481
|
+
const source = this.footnoteRegistry.get(footnoteId);
|
|
2482
|
+
const split = source ? this.splitFootnoteToFit(
|
|
2483
|
+
source,
|
|
2484
|
+
maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
|
|
2485
|
+
ownedDimensions.contentWidth,
|
|
2486
|
+
true
|
|
2487
|
+
) : null;
|
|
2488
|
+
fittingIds.push(footnoteId);
|
|
2489
|
+
if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
|
|
2490
|
+
currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
|
|
2491
|
+
nextPageContinuation = {
|
|
2492
|
+
footnoteId,
|
|
2493
|
+
sourceAnchorId: source.dataset.sourceAnchorId,
|
|
2494
|
+
remainingElements: split.overflow
|
|
2495
|
+
};
|
|
2496
|
+
currentFootnoteHeight = maxFootnoteHeight;
|
|
2497
|
+
} else {
|
|
2498
|
+
currentFootnoteHeight = guardedCandidateHeight;
|
|
2499
|
+
}
|
|
2500
|
+
deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
|
|
2501
|
+
} else {
|
|
2502
|
+
deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
|
|
2503
|
+
}
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
currentFootnoteIds = fittingIds;
|
|
2507
|
+
}
|
|
1333
2508
|
const page = this.createPage(
|
|
1334
|
-
|
|
2509
|
+
ownedDimensions,
|
|
1335
2510
|
pageNumber,
|
|
1336
|
-
|
|
2511
|
+
pageSectionIndex,
|
|
2512
|
+
ownedDisplayedPageNumber,
|
|
1337
2513
|
currentContent,
|
|
1338
|
-
|
|
2514
|
+
ownedPageInSection,
|
|
1339
2515
|
currentFootnoteIds,
|
|
1340
2516
|
currentFootnoteHeight,
|
|
1341
|
-
|
|
1342
|
-
currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
|
|
2517
|
+
pageContinuation,
|
|
2518
|
+
currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
|
|
2519
|
+
false,
|
|
2520
|
+
currentPageAdmitted
|
|
1343
2521
|
);
|
|
1344
2522
|
pages.push(page);
|
|
2523
|
+
precedingDisplayedPageNumber = ownedDisplayedPageNumber;
|
|
1345
2524
|
pageNumber++;
|
|
1346
|
-
pageInSection++;
|
|
1347
2525
|
currentContent = [];
|
|
1348
|
-
|
|
2526
|
+
pageSectionIndex = nextPageSectionIndex;
|
|
2527
|
+
if (!sectionStartPages.has(pageSectionIndex)) {
|
|
2528
|
+
sectionStartPages.set(pageSectionIndex, pageNumber);
|
|
2529
|
+
}
|
|
2530
|
+
const newBands = this.getPageBands(
|
|
2531
|
+
dimensionsFor(),
|
|
2532
|
+
pageSectionIndex,
|
|
2533
|
+
pageInSection(),
|
|
2534
|
+
displayedPageNumber()
|
|
2535
|
+
);
|
|
1349
2536
|
effectiveContentHeight = newBands.bodyHeight;
|
|
1350
2537
|
remainingHeight = effectiveContentHeight;
|
|
1351
2538
|
prevMarginBottomPt = 0;
|
|
1352
2539
|
currentFootnoteIds = [];
|
|
2540
|
+
currentPageHasFootnoteReference = false;
|
|
1353
2541
|
currentPartialFootnotes = [];
|
|
1354
2542
|
currentContinuation = nextPageContinuation;
|
|
1355
2543
|
nextPageContinuation = null;
|
|
2544
|
+
currentContinuationPartitioned = false;
|
|
2545
|
+
currentPageAdmitted = false;
|
|
1356
2546
|
if (deferredFootnoteIds.length > 0) {
|
|
1357
2547
|
currentFootnoteIds = [...deferredFootnoteIds];
|
|
1358
2548
|
deferredFootnoteIds = [];
|
|
1359
2549
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
} else {
|
|
1363
|
-
currentFootnoteHeight = 0;
|
|
1364
|
-
}
|
|
1365
|
-
if (currentFootnoteIds.length > 0) {
|
|
1366
|
-
currentFootnoteHeight += this.measureFootnotesHeight(
|
|
1367
|
-
currentFootnoteIds,
|
|
1368
|
-
dims.contentWidth,
|
|
1369
|
-
null
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
2550
|
+
const nextDimensions = dimensionsFor();
|
|
2551
|
+
currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
|
|
1372
2552
|
};
|
|
2553
|
+
const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1373
2554
|
for (let i = 0; i < blocks.length; i++) {
|
|
2555
|
+
this.checkpoint();
|
|
1374
2556
|
const block = blocks[i];
|
|
2557
|
+
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
2558
|
+
if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
|
|
2559
|
+
finishPage(block.sectionIndex);
|
|
2560
|
+
}
|
|
2561
|
+
const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
|
|
2562
|
+
if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
|
|
2563
|
+
adoptEmptyPageOwner(block.sectionIndex);
|
|
2564
|
+
}
|
|
2565
|
+
prepareCurrentContinuation();
|
|
2566
|
+
if (currentFootnoteHeight > 0) {
|
|
2567
|
+
const ownerDimensions = dimensionsFor();
|
|
2568
|
+
const ownerBands = this.getPageBands(
|
|
2569
|
+
ownerDimensions,
|
|
2570
|
+
pageSectionIndex,
|
|
2571
|
+
pageInSection(),
|
|
2572
|
+
displayedPageNumber()
|
|
2573
|
+
);
|
|
2574
|
+
const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2575
|
+
if (currentFootnoteHeight > ownerMaxFootnoteArea) {
|
|
2576
|
+
const fittingIds = [];
|
|
2577
|
+
let fittingHeight = 0;
|
|
2578
|
+
if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
|
|
2579
|
+
for (const footnoteId of currentFootnoteIds) {
|
|
2580
|
+
this.checkpoint();
|
|
2581
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2582
|
+
[...fittingIds, footnoteId],
|
|
2583
|
+
ownerDimensions.contentWidth
|
|
2584
|
+
);
|
|
2585
|
+
if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
|
|
2586
|
+
fittingIds.push(footnoteId);
|
|
2587
|
+
fittingHeight = candidateHeight;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (fittingIds.length > 0) {
|
|
2591
|
+
deferredFootnoteIds = [
|
|
2592
|
+
...currentFootnoteIds.slice(fittingIds.length),
|
|
2593
|
+
...deferredFootnoteIds
|
|
2594
|
+
];
|
|
2595
|
+
currentFootnoteIds = fittingIds;
|
|
2596
|
+
currentFootnoteHeight = fittingHeight;
|
|
2597
|
+
} else {
|
|
2598
|
+
finishPage(block.sectionIndex);
|
|
2599
|
+
i--;
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const blockDimensions = dimensionsFor(block.sectionIndex);
|
|
2605
|
+
const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
|
|
2606
|
+
const freshBlockPageBodyHeight = this.getPageBands(
|
|
2607
|
+
blockDimensions,
|
|
2608
|
+
block.sectionIndex,
|
|
2609
|
+
nextBlockPageInSection,
|
|
2610
|
+
displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
|
|
2611
|
+
).bodyHeight;
|
|
1375
2612
|
if (block.isPageBreak) {
|
|
1376
|
-
finishPage();
|
|
2613
|
+
finishPage(block.sectionIndex);
|
|
1377
2614
|
continue;
|
|
1378
2615
|
}
|
|
1379
2616
|
if (block.pageBreakBefore && currentContent.length > 0) {
|
|
1380
|
-
finishPage();
|
|
2617
|
+
finishPage(block.sectionIndex);
|
|
1381
2618
|
}
|
|
1382
2619
|
const previousBlock = blocks[i - 1];
|
|
1383
2620
|
const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
|
|
@@ -1393,7 +2630,7 @@ var DocxodusEditor = (() => {
|
|
|
1393
2630
|
if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1394
2631
|
const totalChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1395
2632
|
[...currentFootnoteIds, ...newChainFootnoteIds],
|
|
1396
|
-
|
|
2633
|
+
blockDimensions.contentWidth,
|
|
1397
2634
|
currentContinuation
|
|
1398
2635
|
);
|
|
1399
2636
|
additionalChainFootnoteHeight = Math.max(
|
|
@@ -1405,61 +2642,68 @@ var DocxodusEditor = (() => {
|
|
|
1405
2642
|
keepChain,
|
|
1406
2643
|
prevMarginBottomPt,
|
|
1407
2644
|
currentContent.length === 0,
|
|
1408
|
-
pageInSection
|
|
2645
|
+
pageInSection(block.sectionIndex)
|
|
1409
2646
|
) + additionalChainFootnoteHeight;
|
|
1410
2647
|
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1411
2648
|
if (currentChainHeight > currentAvailableHeight) {
|
|
1412
2649
|
const nextPageBands = this.getPageBands(
|
|
1413
|
-
|
|
1414
|
-
sectionIndex,
|
|
1415
|
-
pageInSection + 1,
|
|
1416
|
-
|
|
2650
|
+
dimensionsFor(block.sectionIndex),
|
|
2651
|
+
block.sectionIndex,
|
|
2652
|
+
pageInSection(block.sectionIndex, pageNumber + 1),
|
|
2653
|
+
displayedPageNumber(
|
|
2654
|
+
block.sectionIndex,
|
|
2655
|
+
pageInSection(block.sectionIndex, pageNumber + 1),
|
|
2656
|
+
pageNumber + 1
|
|
2657
|
+
)
|
|
1417
2658
|
);
|
|
1418
2659
|
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1419
2660
|
keepChain,
|
|
1420
2661
|
0,
|
|
1421
2662
|
true,
|
|
1422
|
-
pageInSection + 1
|
|
2663
|
+
pageInSection(block.sectionIndex, pageNumber + 1)
|
|
1423
2664
|
);
|
|
1424
2665
|
const freshChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1425
2666
|
newChainFootnoteIds,
|
|
1426
|
-
|
|
2667
|
+
blockDimensions.contentWidth,
|
|
1427
2668
|
nextPageContinuation
|
|
1428
2669
|
);
|
|
1429
2670
|
if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
|
|
1430
|
-
finishPage();
|
|
2671
|
+
finishPage(block.sectionIndex);
|
|
1431
2672
|
}
|
|
1432
2673
|
}
|
|
1433
2674
|
}
|
|
1434
2675
|
}
|
|
1435
|
-
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
1436
2676
|
const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
|
|
2677
|
+
let combinedFootnoteHeight = currentFootnoteHeight;
|
|
1437
2678
|
let additionalFootnoteHeight = 0;
|
|
1438
2679
|
if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1439
2680
|
const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
|
|
1440
|
-
|
|
2681
|
+
combinedFootnoteHeight = this.measureFootnotesHeight(
|
|
1441
2682
|
combinedFootnoteIds,
|
|
1442
|
-
|
|
1443
|
-
currentContinuation
|
|
2683
|
+
blockDimensions.contentWidth,
|
|
2684
|
+
currentContinuation,
|
|
2685
|
+
currentPartialFootnotes
|
|
2686
|
+
);
|
|
2687
|
+
additionalFootnoteHeight = Math.max(
|
|
2688
|
+
0,
|
|
2689
|
+
combinedFootnoteHeight - currentFootnoteHeight
|
|
1444
2690
|
);
|
|
1445
|
-
additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
|
|
1446
2691
|
}
|
|
1447
2692
|
const isFirstOnPage = currentContent.length === 0;
|
|
1448
2693
|
const effectiveMarginTop = this.effectiveBlockMarginTop(
|
|
1449
2694
|
block,
|
|
1450
2695
|
prevMarginBottomPt,
|
|
1451
2696
|
isFirstOnPage,
|
|
1452
|
-
pageInSection
|
|
2697
|
+
pageInSection(block.sectionIndex)
|
|
1453
2698
|
);
|
|
1454
2699
|
const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
|
|
1455
2700
|
const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
|
|
1456
2701
|
const bodyContentUsed = effectiveContentHeight - remainingHeight;
|
|
1457
2702
|
const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
1458
|
-
const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
|
|
1459
2703
|
if (blockSpace > effectiveRemainingHeight) {
|
|
1460
2704
|
const paragraphFragments = this.tryFragmentParagraph(
|
|
1461
2705
|
block,
|
|
1462
|
-
|
|
2706
|
+
blockDimensions,
|
|
1463
2707
|
effectiveRemainingHeight,
|
|
1464
2708
|
effectiveMarginTop
|
|
1465
2709
|
);
|
|
@@ -1469,40 +2713,74 @@ var DocxodusEditor = (() => {
|
|
|
1469
2713
|
continue;
|
|
1470
2714
|
}
|
|
1471
2715
|
}
|
|
1472
|
-
if (blockSpace <= effectiveRemainingHeight) {
|
|
1473
|
-
|
|
2716
|
+
if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
|
|
2717
|
+
markSectionPlaced(block.sectionIndex);
|
|
2718
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2719
|
+
block,
|
|
2720
|
+
isFirstOnPage,
|
|
2721
|
+
pageInSection(block.sectionIndex)
|
|
2722
|
+
));
|
|
1474
2723
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
1475
2724
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1476
2725
|
if (newFootnoteIds.length > 0) {
|
|
1477
2726
|
currentFootnoteIds.push(...newFootnoteIds);
|
|
1478
|
-
currentFootnoteHeight
|
|
2727
|
+
currentFootnoteHeight = combinedFootnoteHeight;
|
|
1479
2728
|
}
|
|
1480
|
-
|
|
2729
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
2730
|
+
} else if (block.heightPt + this.effectiveBlockMarginTop(
|
|
2731
|
+
block,
|
|
2732
|
+
0,
|
|
2733
|
+
true,
|
|
2734
|
+
pageInSection(block.sectionIndex, pageNumber + 1)
|
|
2735
|
+
) <= freshBlockPageBodyHeight) {
|
|
1481
2736
|
const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
|
|
1482
|
-
const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
|
|
1483
|
-
const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
|
|
1484
2737
|
if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
|
|
1485
|
-
|
|
2738
|
+
markSectionPlaced(block.sectionIndex);
|
|
2739
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2740
|
+
block,
|
|
2741
|
+
isFirstOnPage,
|
|
2742
|
+
pageInSection(block.sectionIndex)
|
|
2743
|
+
));
|
|
1486
2744
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
1487
2745
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1488
2746
|
const availableForFootnotes = Math.min(
|
|
1489
2747
|
maxFootnoteArea,
|
|
1490
2748
|
effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
|
|
1491
2749
|
);
|
|
1492
|
-
|
|
2750
|
+
const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2751
|
+
const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
|
|
2752
|
+
for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
|
|
2753
|
+
this.checkpoint();
|
|
2754
|
+
const footnoteId = newFootnoteIds[noteIndex];
|
|
1493
2755
|
const footnote = this.footnoteRegistry.get(footnoteId);
|
|
1494
2756
|
if (!footnote) continue;
|
|
1495
|
-
|
|
2757
|
+
if (nextPageContinuation) {
|
|
2758
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2759
|
+
break;
|
|
2760
|
+
}
|
|
2761
|
+
const candidateIds = [...currentFootnoteIds, footnoteId];
|
|
2762
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2763
|
+
candidateIds,
|
|
2764
|
+
blockDimensions.contentWidth,
|
|
2765
|
+
currentContinuation,
|
|
2766
|
+
currentPartialFootnotes
|
|
2767
|
+
);
|
|
1496
2768
|
const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
|
|
1497
|
-
if (
|
|
2769
|
+
if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
|
|
1498
2770
|
currentFootnoteIds.push(footnoteId);
|
|
1499
|
-
currentFootnoteHeight
|
|
2771
|
+
currentFootnoteHeight = candidateHeight;
|
|
1500
2772
|
} else {
|
|
1501
2773
|
if (spaceLeftForFootnotes > 20) {
|
|
1502
2774
|
const { fits, overflow } = this.splitFootnoteToFit(
|
|
1503
2775
|
footnote,
|
|
1504
|
-
|
|
1505
|
-
|
|
2776
|
+
availableForFootnotes,
|
|
2777
|
+
blockDimensions.contentWidth,
|
|
2778
|
+
deferralCannotHelp,
|
|
2779
|
+
{
|
|
2780
|
+
footnoteIds: currentFootnoteIds,
|
|
2781
|
+
continuation: currentContinuation,
|
|
2782
|
+
partialFootnotes: currentPartialFootnotes
|
|
2783
|
+
}
|
|
1506
2784
|
);
|
|
1507
2785
|
if (fits.length > 0) {
|
|
1508
2786
|
currentFootnoteIds.push(footnoteId);
|
|
@@ -1513,78 +2791,70 @@ var DocxodusEditor = (() => {
|
|
|
1513
2791
|
if (overflow.length > 0) {
|
|
1514
2792
|
nextPageContinuation = {
|
|
1515
2793
|
footnoteId,
|
|
2794
|
+
sourceAnchorId: footnote.dataset.sourceAnchorId,
|
|
1516
2795
|
remainingElements: overflow
|
|
1517
2796
|
};
|
|
1518
2797
|
}
|
|
1519
|
-
currentFootnoteHeight =
|
|
2798
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2799
|
+
currentFootnoteIds,
|
|
2800
|
+
blockDimensions.contentWidth,
|
|
2801
|
+
currentContinuation,
|
|
2802
|
+
currentPartialFootnotes
|
|
2803
|
+
);
|
|
2804
|
+
if (overflow.length > 0) {
|
|
2805
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
|
|
2806
|
+
break;
|
|
2807
|
+
}
|
|
1520
2808
|
} else {
|
|
1521
|
-
deferredFootnoteIds.push(
|
|
2809
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2810
|
+
break;
|
|
1522
2811
|
}
|
|
1523
2812
|
} else {
|
|
1524
|
-
deferredFootnoteIds.push(
|
|
2813
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2814
|
+
break;
|
|
1525
2815
|
}
|
|
1526
2816
|
}
|
|
1527
2817
|
}
|
|
1528
|
-
|
|
1529
|
-
const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
|
|
1530
|
-
const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
|
|
1531
|
-
if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
|
|
1532
|
-
currentContent.push(this.cloneBlockForPage(block, isFirstOnPage, pageInSection));
|
|
1533
|
-
remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
|
|
1534
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1535
|
-
currentFootnoteIds.push(...newFootnoteIds);
|
|
1536
|
-
currentFootnoteHeight = expandedFootnoteSpace;
|
|
1537
|
-
} else {
|
|
1538
|
-
finishPage();
|
|
1539
|
-
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
1540
|
-
const newPageMarginTop = this.effectiveBlockMarginTop(
|
|
1541
|
-
block,
|
|
1542
|
-
0,
|
|
1543
|
-
true,
|
|
1544
|
-
pageInSection
|
|
1545
|
-
);
|
|
1546
|
-
const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
|
|
1547
|
-
currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
|
|
1548
|
-
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1549
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1550
|
-
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1551
|
-
currentFootnoteHeight = newPageFootnoteHeight;
|
|
1552
|
-
}
|
|
2818
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
1553
2819
|
} else {
|
|
1554
|
-
finishPage();
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
block,
|
|
1558
|
-
0,
|
|
1559
|
-
true,
|
|
1560
|
-
pageInSection
|
|
1561
|
-
);
|
|
1562
|
-
const newPageSpace = newPageMarginTop + block.heightPt + block.marginBottomPt;
|
|
1563
|
-
currentContent.push(this.cloneBlockForPage(block, true, pageInSection));
|
|
1564
|
-
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1565
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1566
|
-
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1567
|
-
currentFootnoteHeight = newPageFootnoteHeight;
|
|
2820
|
+
finishPage(block.sectionIndex, !pageHasPayload());
|
|
2821
|
+
i--;
|
|
2822
|
+
continue;
|
|
1568
2823
|
}
|
|
1569
2824
|
} else {
|
|
1570
|
-
const tableFragments = this.trySplitSimpleOversizedTable(
|
|
2825
|
+
const tableFragments = this.trySplitSimpleOversizedTable(
|
|
2826
|
+
block,
|
|
2827
|
+
dimensionsFor(block.sectionIndex),
|
|
2828
|
+
block.sectionIndex
|
|
2829
|
+
);
|
|
1571
2830
|
if (tableFragments) {
|
|
1572
2831
|
if (currentContent.length > 0 || currentContinuation) {
|
|
1573
|
-
finishPage();
|
|
2832
|
+
finishPage(block.sectionIndex);
|
|
1574
2833
|
}
|
|
1575
2834
|
blocks.splice(i, 1, ...tableFragments);
|
|
1576
2835
|
i--;
|
|
1577
2836
|
continue;
|
|
1578
2837
|
}
|
|
1579
|
-
if (
|
|
1580
|
-
finishPage();
|
|
2838
|
+
if (pageHasPayload()) {
|
|
2839
|
+
finishPage(block.sectionIndex);
|
|
2840
|
+
i--;
|
|
2841
|
+
continue;
|
|
1581
2842
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
2843
|
+
markSectionPlaced(block.sectionIndex);
|
|
2844
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2845
|
+
block,
|
|
2846
|
+
true,
|
|
2847
|
+
pageInSection(block.sectionIndex)
|
|
2848
|
+
));
|
|
2849
|
+
deferredFootnoteIds.push(...newFootnoteIds);
|
|
2850
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
2851
|
+
finishPage(block.sectionIndex);
|
|
1585
2852
|
}
|
|
1586
2853
|
}
|
|
1587
2854
|
finishPage();
|
|
2855
|
+
while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
|
|
2856
|
+
finishPage();
|
|
2857
|
+
}
|
|
1588
2858
|
this.pendingFootnoteContinuation = nextPageContinuation;
|
|
1589
2859
|
return pages;
|
|
1590
2860
|
}
|
|
@@ -1609,6 +2879,19 @@ var DocxodusEditor = (() => {
|
|
|
1609
2879
|
if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
|
|
1610
2880
|
}
|
|
1611
2881
|
}
|
|
2882
|
+
/** A repeated margin note is presentation, not a second bookmark/link target. */
|
|
2883
|
+
makeClonedMarginCommentInert(root) {
|
|
2884
|
+
this.makeClonedStoryInert(root);
|
|
2885
|
+
const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
2886
|
+
for (const element of nodes) {
|
|
2887
|
+
element.removeAttribute("id");
|
|
2888
|
+
if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
|
|
2889
|
+
element.removeAttribute("href");
|
|
2890
|
+
element.setAttribute("aria-disabled", "true");
|
|
2891
|
+
element.tabIndex = -1;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
1612
2895
|
/**
|
|
1613
2896
|
* Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
|
|
1614
2897
|
*
|
|
@@ -1634,7 +2917,7 @@ var DocxodusEditor = (() => {
|
|
|
1634
2917
|
const context = {
|
|
1635
2918
|
staticLeft: toPageX(staticRect.left),
|
|
1636
2919
|
staticTop: toPageY(staticRect.top),
|
|
1637
|
-
lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
|
|
2920
|
+
lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
|
|
1638
2921
|
paragraphLeft: toPageX(paragraphRect.left),
|
|
1639
2922
|
paragraphTop: toPageY(paragraphRect.top),
|
|
1640
2923
|
paragraphWidth: paragraphRect.width / pixelsPerPoint,
|
|
@@ -1764,36 +3047,48 @@ var DocxodusEditor = (() => {
|
|
|
1764
3047
|
else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
|
|
1765
3048
|
return reference.start + (reference.size - objectSize) * fraction;
|
|
1766
3049
|
}
|
|
3050
|
+
/** Charge a physical page before any page-owned partitioning or DOM allocation. */
|
|
3051
|
+
admitPageAllocation() {
|
|
3052
|
+
const prospectivePageCount = this.createdPageCount + 1;
|
|
3053
|
+
this.pageCountCheckpoint?.(prospectivePageCount);
|
|
3054
|
+
this.createdPageCount = prospectivePageCount;
|
|
3055
|
+
}
|
|
1767
3056
|
/**
|
|
1768
3057
|
* Creates a page container element.
|
|
1769
3058
|
*/
|
|
1770
|
-
createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
|
|
1771
|
-
|
|
3059
|
+
createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
|
|
3060
|
+
if (!pageAlreadyAdmitted) this.admitPageAllocation();
|
|
3061
|
+
const pageBox = this.document.createElement("div");
|
|
1772
3062
|
pageBox.className = `${this.cssPrefix}box`;
|
|
1773
3063
|
pageBox.style.width = `${dims.pageWidth}pt`;
|
|
1774
3064
|
pageBox.style.height = `${dims.pageHeight}pt`;
|
|
1775
3065
|
pageBox.style.overflow = "hidden";
|
|
1776
3066
|
pageBox.style.position = "relative";
|
|
1777
3067
|
if (this.scale !== 1) {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
3068
|
+
if (this.view.CSS?.supports("zoom", "1")) {
|
|
3069
|
+
pageBox.style.zoom = String(this.scale);
|
|
3070
|
+
} else {
|
|
3071
|
+
pageBox.style.transform = `scale(${this.scale})`;
|
|
3072
|
+
pageBox.style.transformOrigin = "top left";
|
|
3073
|
+
const heightReductionPt = dims.pageHeight * (1 - this.scale);
|
|
3074
|
+
const widthReductionPt = dims.pageWidth * (1 - this.scale);
|
|
3075
|
+
const heightReductionPx = ptToPx(heightReductionPt);
|
|
3076
|
+
const widthReductionPx = ptToPx(widthReductionPt);
|
|
3077
|
+
pageBox.style.marginRight = `-${widthReductionPx}px`;
|
|
3078
|
+
pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
|
|
3079
|
+
}
|
|
1787
3080
|
}
|
|
1788
3081
|
pageBox.style.willChange = "transform";
|
|
1789
3082
|
pageBox.style.contain = "layout paint";
|
|
1790
3083
|
pageBox.dataset.pageNumber = String(pageNumber);
|
|
1791
3084
|
pageBox.dataset.sectionIndex = String(sectionIndex);
|
|
3085
|
+
pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
|
|
3086
|
+
if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
|
|
1792
3087
|
pageBox.dataset.pageInSection = String(pageInSection);
|
|
1793
|
-
const bands = this.getPageBands(dims, sectionIndex, pageInSection,
|
|
1794
|
-
const headerSource = this.selectHeader(sectionIndex, pageInSection,
|
|
3088
|
+
const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
|
|
3089
|
+
const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
|
|
1795
3090
|
if (headerSource) {
|
|
1796
|
-
const headerDiv = document.createElement("div");
|
|
3091
|
+
const headerDiv = this.document.createElement("div");
|
|
1797
3092
|
headerDiv.className = `${this.cssPrefix}header`;
|
|
1798
3093
|
headerDiv.style.position = "absolute";
|
|
1799
3094
|
headerDiv.style.top = `${bands.headerTop}pt`;
|
|
@@ -1815,7 +3110,7 @@ var DocxodusEditor = (() => {
|
|
|
1815
3110
|
}
|
|
1816
3111
|
const contentAreaTop = bands.bodyTop;
|
|
1817
3112
|
const contentAreaHeight = bands.bodyHeight;
|
|
1818
|
-
const contentArea = document.createElement("div");
|
|
3113
|
+
const contentArea = this.document.createElement("div");
|
|
1819
3114
|
contentArea.className = `${this.cssPrefix}content`;
|
|
1820
3115
|
contentArea.style.position = "absolute";
|
|
1821
3116
|
contentArea.style.top = `${contentAreaTop}pt`;
|
|
@@ -1828,12 +3123,46 @@ var DocxodusEditor = (() => {
|
|
|
1828
3123
|
}
|
|
1829
3124
|
pageBox.appendChild(contentArea);
|
|
1830
3125
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
1831
|
-
if (footnoteIds.length > 0 || hasContinuation) {
|
|
3126
|
+
if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
|
|
1832
3127
|
this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
|
|
1833
3128
|
}
|
|
1834
|
-
const
|
|
3129
|
+
const pageCommentIds = [];
|
|
3130
|
+
for (const marker of Array.from(
|
|
3131
|
+
pageBox.querySelectorAll("[data-comment-id]")
|
|
3132
|
+
)) {
|
|
3133
|
+
if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
|
|
3134
|
+
const id = marker.dataset.commentId;
|
|
3135
|
+
if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
|
|
3136
|
+
pageCommentIds.push(id);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (pageCommentIds.length > 0) {
|
|
3140
|
+
const marginColumn = this.document.createElement("aside");
|
|
3141
|
+
marginColumn.className = `${this.cssPrefix}comment-margin`;
|
|
3142
|
+
marginColumn.style.position = "absolute";
|
|
3143
|
+
marginColumn.style.top = `${contentAreaTop}pt`;
|
|
3144
|
+
marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
|
|
3145
|
+
marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
|
|
3146
|
+
marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
|
|
3147
|
+
marginColumn.style.overflow = "hidden";
|
|
3148
|
+
marginColumn.style.boxSizing = "border-box";
|
|
3149
|
+
const placedThreadRoots = /* @__PURE__ */ new Set();
|
|
3150
|
+
for (const id of pageCommentIds) {
|
|
3151
|
+
const source = this.commentMarginRegistry.get(id);
|
|
3152
|
+
if (source) {
|
|
3153
|
+
const rootId = source.dataset.commentId ?? id;
|
|
3154
|
+
if (placedThreadRoots.has(rootId)) continue;
|
|
3155
|
+
placedThreadRoots.add(rootId);
|
|
3156
|
+
const clone = source.cloneNode(true);
|
|
3157
|
+
this.makeClonedMarginCommentInert(clone);
|
|
3158
|
+
marginColumn.appendChild(clone);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
pageBox.appendChild(marginColumn);
|
|
3162
|
+
}
|
|
3163
|
+
const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
|
|
1835
3164
|
if (footerSource) {
|
|
1836
|
-
const footerDiv = document.createElement("div");
|
|
3165
|
+
const footerDiv = this.document.createElement("div");
|
|
1837
3166
|
footerDiv.className = `${this.cssPrefix}footer`;
|
|
1838
3167
|
footerDiv.style.position = "absolute";
|
|
1839
3168
|
footerDiv.style.top = "auto";
|
|
@@ -1853,8 +3182,8 @@ var DocxodusEditor = (() => {
|
|
|
1853
3182
|
}
|
|
1854
3183
|
pageBox.appendChild(footerDiv);
|
|
1855
3184
|
}
|
|
1856
|
-
if (this.showPageNumbers) {
|
|
1857
|
-
const pageNum = document.createElement("div");
|
|
3185
|
+
if (this.showPageNumbers && !isSectionFiller) {
|
|
3186
|
+
const pageNum = this.document.createElement("div");
|
|
1858
3187
|
pageNum.className = `${this.cssPrefix}number`;
|
|
1859
3188
|
pageNum.textContent = String(pageNumber);
|
|
1860
3189
|
pageBox.appendChild(pageNum);
|
|
@@ -1877,7 +3206,8 @@ var DocxodusEditor = (() => {
|
|
|
1877
3206
|
}
|
|
1878
3207
|
};
|
|
1879
3208
|
function paginateHtml(html, container, options = {}) {
|
|
1880
|
-
const
|
|
3209
|
+
const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
|
|
3210
|
+
const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
|
|
1881
3211
|
if (!containerEl) {
|
|
1882
3212
|
throw new Error("Container element not found");
|
|
1883
3213
|
}
|
|
@@ -5179,6 +6509,13 @@ var DocxodusEditor = (() => {
|
|
|
5179
6509
|
function escapeInlineMarkdown(text) {
|
|
5180
6510
|
return text.replace(/([\\`*_[\]])/g, "\\$1");
|
|
5181
6511
|
}
|
|
6512
|
+
function escapeLeadingBlockMarkers(md) {
|
|
6513
|
+
return md.split("\n").map((line) => {
|
|
6514
|
+
if (/^[#>|]/.test(line) || /^[-+] /.test(line)) return "\\" + line;
|
|
6515
|
+
if (/^\d+\. /.test(line)) return line.replace(".", "\\.");
|
|
6516
|
+
return line;
|
|
6517
|
+
}).join("\n");
|
|
6518
|
+
}
|
|
5182
6519
|
function collectInlineSegments(node, out) {
|
|
5183
6520
|
node.childNodes.forEach((child) => {
|
|
5184
6521
|
if (isGeneratedChrome(child)) return;
|
|
@@ -5230,7 +6567,7 @@ var DocxodusEditor = (() => {
|
|
|
5230
6567
|
merged.push({ ...s });
|
|
5231
6568
|
}
|
|
5232
6569
|
}
|
|
5233
|
-
return merged.map(segToMarkdown).join("").trim();
|
|
6570
|
+
return escapeLeadingBlockMarkers(merged.map(segToMarkdown).join("").trim());
|
|
5234
6571
|
}
|
|
5235
6572
|
function isListBlock(block) {
|
|
5236
6573
|
return !!block.querySelector(":scope > [data-list-marker]");
|
|
@@ -7265,17 +8602,19 @@ var DocxodusEditor = (() => {
|
|
|
7265
8602
|
this.refreshAfter(block, idx, false);
|
|
7266
8603
|
}
|
|
7267
8604
|
// ─── Table row / column editing (active block must be inside a table cell) ──────────
|
|
7268
|
-
/** Run a table-structure op on the active cell
|
|
8605
|
+
/** Run a table-structure op on the active cell's canonical tc anchor and re-render. */
|
|
7269
8606
|
tableEdit(run) {
|
|
7270
8607
|
const block = this.activeBlock;
|
|
7271
|
-
|
|
8608
|
+
const cell = block?.closest("td, th");
|
|
8609
|
+
if (this.closed || !block || !cell || !block.closest("table")) return;
|
|
7272
8610
|
const unid = block.getAttribute("data-anchor");
|
|
7273
8611
|
if (!unid) return;
|
|
7274
|
-
|
|
7275
|
-
|
|
8612
|
+
const paragraphId = this.anchorIdOf(block);
|
|
8613
|
+
const cellId = this.anchorIdOf(cell);
|
|
8614
|
+
if (!paragraphId || !cellId) return;
|
|
7276
8615
|
const idx = this.blockIndex(block);
|
|
7277
|
-
|
|
7278
|
-
const res = this.parseEdit(run(
|
|
8616
|
+
this.syncBlock(block, paragraphId);
|
|
8617
|
+
const res = this.parseEdit(run(cellId));
|
|
7279
8618
|
if (!res.success) return;
|
|
7280
8619
|
this.refreshAfter(block, idx, false);
|
|
7281
8620
|
}
|
|
@@ -7428,6 +8767,59 @@ var DocxodusEditor = (() => {
|
|
|
7428
8767
|
if (!res.success) return;
|
|
7429
8768
|
this.reconcile(idx, false);
|
|
7430
8769
|
}
|
|
8770
|
+
/**
|
|
8771
|
+
* Add a native Word comment on the current selection in the active block — or on the whole
|
|
8772
|
+
* block when the selection is collapsed (a null span comments the paragraph). The definition,
|
|
8773
|
+
* threading parts, and body-side range markers are written by the session's `AddComment`
|
|
8774
|
+
* (issue #300); the re-render shows the marker chrome the converter already draws. The
|
|
8775
|
+
* annotation type legal review runs on, finally authorable from the shipped surface
|
|
8776
|
+
* (issue #580).
|
|
8777
|
+
*/
|
|
8778
|
+
addComment(markdown = "New comment.", author = "Reviewer") {
|
|
8779
|
+
const block = this.activeBlock;
|
|
8780
|
+
if (this.closed || !block) return;
|
|
8781
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8782
|
+
if (!bridge.AddComment) return;
|
|
8783
|
+
let fullId = this.anchorIdOf(block);
|
|
8784
|
+
if (!fullId) return;
|
|
8785
|
+
const idx = this.blockIndex(block);
|
|
8786
|
+
const span = selectionSpanIn(block);
|
|
8787
|
+
fullId = this.syncBlock(block, fullId);
|
|
8788
|
+
const res = this.parseEdit(
|
|
8789
|
+
bridge.AddComment(
|
|
8790
|
+
this.handle,
|
|
8791
|
+
fullId,
|
|
8792
|
+
span ? JSON.stringify(span) : "",
|
|
8793
|
+
author,
|
|
8794
|
+
"",
|
|
8795
|
+
"",
|
|
8796
|
+
markdown
|
|
8797
|
+
)
|
|
8798
|
+
);
|
|
8799
|
+
if (!res.success) return;
|
|
8800
|
+
this.reconcile(idx, false);
|
|
8801
|
+
}
|
|
8802
|
+
/** The document's native comment threads (session truth), for review UIs — flat entries
|
|
8803
|
+
* with `parentAnchorId` linking replies and `resolved` carrying thread state. */
|
|
8804
|
+
listComments() {
|
|
8805
|
+
if (this.closed) return [];
|
|
8806
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8807
|
+
if (!bridge.ListComments) return [];
|
|
8808
|
+
try {
|
|
8809
|
+
return JSON.parse(bridge.ListComments(this.handle));
|
|
8810
|
+
} catch {
|
|
8811
|
+
return [];
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
/** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}).
|
|
8815
|
+
* Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is
|
|
8816
|
+
* needed — a review UI re-reads {@link listComments} for the new state. */
|
|
8817
|
+
setCommentResolved(commentAnchorId, resolved) {
|
|
8818
|
+
if (this.closed) return;
|
|
8819
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8820
|
+
if (!bridge.SetCommentResolved) return;
|
|
8821
|
+
this.parseEdit(bridge.SetCommentResolved(this.handle, commentAnchorId, resolved));
|
|
8822
|
+
}
|
|
7431
8823
|
applyParagraphFormat(op) {
|
|
7432
8824
|
const block = this.activeBlock;
|
|
7433
8825
|
if (this.closed || !block) return;
|
|
@@ -7955,8 +9347,8 @@ var DocxodusEditor = (() => {
|
|
|
7955
9347
|
const backref = document.createElement("a");
|
|
7956
9348
|
backref.setAttribute("class", `${prefix}-backref`);
|
|
7957
9349
|
backref.setAttribute("contenteditable", "false");
|
|
7958
|
-
backref.textContent = "\u21A9";
|
|
7959
|
-
last.append(
|
|
9350
|
+
backref.textContent = " \u21A9";
|
|
9351
|
+
last.append(backref);
|
|
7960
9352
|
}
|
|
7961
9353
|
li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
|
|
7962
9354
|
return li;
|
|
@@ -8513,6 +9905,25 @@ var DocxodusEditor = (() => {
|
|
|
8513
9905
|
box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
|
|
8514
9906
|
}
|
|
8515
9907
|
|
|
9908
|
+
/* \u2500\u2500 Review: comment threads \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
9909
|
+
.dxr-threads {
|
|
9910
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
9911
|
+
max-height: 92px; overflow-y: auto; min-width: 260px; padding-right: 2px;
|
|
9912
|
+
}
|
|
9913
|
+
.dxr-thread {
|
|
9914
|
+
display: flex; align-items: center; gap: 6px;
|
|
9915
|
+
font-size: 11px; line-height: 1.3; padding: 3px 6px;
|
|
9916
|
+
border: 1px solid var(--dxr-rule); border-radius: 4px; background: var(--dxr-chrome-sunk);
|
|
9917
|
+
}
|
|
9918
|
+
.dxr-thread[data-resolved] { opacity: 0.55; }
|
|
9919
|
+
.dxr-thread[data-reply] { margin-left: 14px; }
|
|
9920
|
+
.dxr-thread .dxr-tauthor { font-weight: 600; white-space: nowrap; }
|
|
9921
|
+
.dxr-thread .dxr-ttext {
|
|
9922
|
+
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
|
|
9923
|
+
}
|
|
9924
|
+
.dxr-thread button { flex: none; }
|
|
9925
|
+
.dxr-group.dxr-grow { flex: 1; min-width: 0; }
|
|
9926
|
+
|
|
8516
9927
|
/* \u2500\u2500 Table size picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
8517
9928
|
.dxr-pop {
|
|
8518
9929
|
display: none;
|
|
@@ -8831,6 +10242,7 @@ var DocxodusEditor = (() => {
|
|
|
8831
10242
|
<button type="button" class="dxr-tab" role="tab" data-tab="home" aria-selected="true">Home</button>
|
|
8832
10243
|
<button type="button" class="dxr-tab" role="tab" data-tab="insert" aria-selected="false">Insert</button>
|
|
8833
10244
|
<button type="button" class="dxr-tab" role="tab" data-tab="layout" aria-selected="false">Layout</button>
|
|
10245
|
+
<button type="button" class="dxr-tab" role="tab" data-tab="review" aria-selected="false">Review</button>
|
|
8834
10246
|
<button type="button" class="dxr-tab" role="tab" data-tab="table" data-contextual aria-selected="false" hidden>Table</button>
|
|
8835
10247
|
</div>
|
|
8836
10248
|
|
|
@@ -8996,6 +10408,31 @@ var DocxodusEditor = (() => {
|
|
|
8996
10408
|
</div>
|
|
8997
10409
|
</div>
|
|
8998
10410
|
|
|
10411
|
+
<!-- REVIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
10412
|
+
Comment authoring over the session's native comment family (issue #580):
|
|
10413
|
+
the button comments the selection (the whole block when collapsed), and
|
|
10414
|
+
the thread list is live session truth with resolve/reopen per thread. -->
|
|
10415
|
+
<div class="dxr-panel" data-panel="review">
|
|
10416
|
+
<div class="dxr-group">
|
|
10417
|
+
<span class="dxr-glabel">Comments</span>
|
|
10418
|
+
<div class="dxr-row">
|
|
10419
|
+
<input data-dxr="commenttext" type="text" placeholder="Comment text…"
|
|
10420
|
+
title="The comment body; markdown is honored" />
|
|
10421
|
+
<button type="button" data-dxr="comment"
|
|
10422
|
+
title="Comment on the selection, or on the whole block when nothing is selected">💬 Comment</button>
|
|
10423
|
+
</div>
|
|
10424
|
+
<div class="dxr-row">
|
|
10425
|
+
<span class="dxr-note">Comments the selected text; the whole block when the caret is collapsed.</span>
|
|
10426
|
+
</div>
|
|
10427
|
+
</div>
|
|
10428
|
+
<div class="dxr-group dxr-grow">
|
|
10429
|
+
<span class="dxr-glabel">Threads</span>
|
|
10430
|
+
<div class="dxr-threads" data-dxr="threads" aria-live="polite">
|
|
10431
|
+
<span class="dxr-note">No comments yet.</span>
|
|
10432
|
+
</div>
|
|
10433
|
+
</div>
|
|
10434
|
+
</div>
|
|
10435
|
+
|
|
8999
10436
|
<!-- TABLE (contextual) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
9000
10437
|
Row/column editing lives here rather than in a floating toolbar: a docked
|
|
9001
10438
|
contextual tab cannot overlap the cell you are editing. -->
|
|
@@ -9503,6 +10940,18 @@ var DocxodusEditor = (() => {
|
|
|
9503
10940
|
if (this.live && style.value) this.run("style", () => this.live.setParagraphStyle(style.value));
|
|
9504
10941
|
style.value = "";
|
|
9505
10942
|
});
|
|
10943
|
+
const commentButton = this.control("comment");
|
|
10944
|
+
if (commentButton) {
|
|
10945
|
+
this.keepSelection(commentButton);
|
|
10946
|
+
commentButton.addEventListener("click", () => {
|
|
10947
|
+
if (!this.live) return;
|
|
10948
|
+
const input = this.control("commenttext");
|
|
10949
|
+
const text = input?.value.trim() || "New comment.";
|
|
10950
|
+
this.run("comment", () => this.live.addComment(text, this.options.revisionAuthor ?? "Reviewer"));
|
|
10951
|
+
if (input) input.value = "";
|
|
10952
|
+
this.renderThreads();
|
|
10953
|
+
});
|
|
10954
|
+
}
|
|
9506
10955
|
this.wireFileActions();
|
|
9507
10956
|
this.wireLayout();
|
|
9508
10957
|
this.wirePicker();
|
|
@@ -9625,6 +11074,52 @@ var DocxodusEditor = (() => {
|
|
|
9625
11074
|
else panel.removeAttribute("data-active");
|
|
9626
11075
|
}
|
|
9627
11076
|
if (name === "layout") this.syncPageNumbering();
|
|
11077
|
+
if (name === "review") this.renderThreads();
|
|
11078
|
+
}
|
|
11079
|
+
/** Re-render the Review tab's thread list from session truth ({@link DocxEditor.listComments}):
|
|
11080
|
+
* one row per comment, replies indented under their thread root, resolve/reopen per root. */
|
|
11081
|
+
renderThreads() {
|
|
11082
|
+
const host = this.control("threads");
|
|
11083
|
+
if (!host || !this.live) return;
|
|
11084
|
+
const doc = this.element.ownerDocument ?? document;
|
|
11085
|
+
host.textContent = "";
|
|
11086
|
+
const comments = this.live.listComments();
|
|
11087
|
+
if (comments.length === 0) {
|
|
11088
|
+
const empty = doc.createElement("span");
|
|
11089
|
+
empty.className = "dxr-note";
|
|
11090
|
+
empty.textContent = "No comments yet.";
|
|
11091
|
+
host.appendChild(empty);
|
|
11092
|
+
return;
|
|
11093
|
+
}
|
|
11094
|
+
for (const entry of comments) {
|
|
11095
|
+
const row = doc.createElement("div");
|
|
11096
|
+
row.className = "dxr-thread";
|
|
11097
|
+
row.setAttribute("data-thread", entry.anchorId);
|
|
11098
|
+
if (entry.resolved) row.setAttribute("data-resolved", "");
|
|
11099
|
+
if (entry.parentAnchorId) row.setAttribute("data-reply", "");
|
|
11100
|
+
const author = doc.createElement("span");
|
|
11101
|
+
author.className = "dxr-tauthor";
|
|
11102
|
+
author.textContent = entry.author || "\u2014";
|
|
11103
|
+
row.appendChild(author);
|
|
11104
|
+
const text = doc.createElement("span");
|
|
11105
|
+
text.className = "dxr-ttext";
|
|
11106
|
+
text.textContent = entry.text;
|
|
11107
|
+
text.title = entry.text;
|
|
11108
|
+
row.appendChild(text);
|
|
11109
|
+
if (!entry.parentAnchorId) {
|
|
11110
|
+
const toggle = doc.createElement("button");
|
|
11111
|
+
toggle.type = "button";
|
|
11112
|
+
toggle.textContent = entry.resolved ? "Reopen" : "Resolve";
|
|
11113
|
+
toggle.title = entry.resolved ? "Reopen this comment thread" : "Mark this comment thread resolved";
|
|
11114
|
+
toggle.addEventListener("click", () => {
|
|
11115
|
+
if (!this.live) return;
|
|
11116
|
+
this.run(entry.resolved ? "reopen comment" : "resolve comment", () => this.live.setCommentResolved(entry.anchorId, !entry.resolved));
|
|
11117
|
+
this.renderThreads();
|
|
11118
|
+
});
|
|
11119
|
+
row.appendChild(toggle);
|
|
11120
|
+
}
|
|
11121
|
+
host.appendChild(row);
|
|
11122
|
+
}
|
|
9628
11123
|
}
|
|
9629
11124
|
// ── selection-driven state ──────────────────────────────────────────────────
|
|
9630
11125
|
selectionElement() {
|