docxodus 7.1.0 → 8.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/dist/docxodus.worker.js +2 -2
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor-headerfooter.d.ts +130 -0
- package/dist/editor-headerfooter.d.ts.map +1 -0
- package/dist/editor-headerfooter.js +414 -0
- package/dist/editor-headerfooter.js.map +1 -0
- package/dist/editor.bundle.js +1075 -81
- package/dist/editor.d.ts +82 -4
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +304 -63
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/pagination.bundle.js +480 -19
- package/dist/pagination.d.ts +81 -0
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +528 -26
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +6 -4
- package/dist/react.js.map +1 -1
- package/dist/session.bundle.js +15 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +15 -0
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +47 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -11
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encoding.Extensions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.Thread.wasm +0 -0
- package/dist/wasm/_framework/System.Threading.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.ReaderWriter.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XPath.wasm +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +41 -41
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.map +1 -1
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.map +1 -1
- package/dist/wasm/index.html +2 -1
- package/package.json +1 -1
package/dist/editor.bundle.js
CHANGED
|
@@ -84,6 +84,7 @@ var DocxodusEditor = (() => {
|
|
|
84
84
|
this.cssPrefix = options.cssPrefix ?? "page-";
|
|
85
85
|
this.showPageNumbers = options.showPageNumbers ?? true;
|
|
86
86
|
this.pageGap = options.pageGap ?? 20;
|
|
87
|
+
this.fragmentParagraphs = options.fragmentParagraphs ?? false;
|
|
87
88
|
this.hfRegistry = /* @__PURE__ */ new Map();
|
|
88
89
|
this.footnoteRegistry = /* @__PURE__ */ new Map();
|
|
89
90
|
}
|
|
@@ -150,6 +151,393 @@ var DocxodusEditor = (() => {
|
|
|
150
151
|
}
|
|
151
152
|
return blocks;
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Measures one element in the same hidden staging context used for the source blocks.
|
|
156
|
+
* This is intentionally DOM-based: table row heights cannot be inferred from individual
|
|
157
|
+
* rows because wrapping and collapsed borders change the height of a fragment.
|
|
158
|
+
*/
|
|
159
|
+
measureElement(element, dims) {
|
|
160
|
+
const measurementHost = document.createElement("div");
|
|
161
|
+
measurementHost.style.position = "absolute";
|
|
162
|
+
measurementHost.style.visibility = "hidden";
|
|
163
|
+
measurementHost.style.left = "-9999px";
|
|
164
|
+
measurementHost.style.width = `${dims.contentWidth}pt`;
|
|
165
|
+
const measuredElement = element.cloneNode(true);
|
|
166
|
+
measurementHost.appendChild(measuredElement);
|
|
167
|
+
this.stagingElement.appendChild(measurementHost);
|
|
168
|
+
const rect = measuredElement.getBoundingClientRect();
|
|
169
|
+
const style = window.getComputedStyle(measuredElement);
|
|
170
|
+
const measured = {
|
|
171
|
+
element,
|
|
172
|
+
heightPt: pxToPt(rect.height),
|
|
173
|
+
marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
|
|
174
|
+
marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
|
|
175
|
+
keepWithNext: element.dataset.keepWithNext === "true",
|
|
176
|
+
keepLines: element.dataset.keepLines === "true",
|
|
177
|
+
pageBreakBefore: element.dataset.pageBreakBefore === "true",
|
|
178
|
+
isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
|
|
179
|
+
};
|
|
180
|
+
this.stagingElement.removeChild(measurementHost);
|
|
181
|
+
return measured;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Returns the contiguous keep-with-next chain beginning at a block.
|
|
185
|
+
*
|
|
186
|
+
* A hard page break or a page-break-before directive is stronger than a
|
|
187
|
+
* keep-with-next directive, so it terminates the chain. The caller only
|
|
188
|
+
* keeps a chain together when the whole chain can fit on a fresh page.
|
|
189
|
+
*/
|
|
190
|
+
getKeepWithNextChain(blocks, startIndex) {
|
|
191
|
+
const firstBlock = blocks[startIndex];
|
|
192
|
+
if (!firstBlock) return [];
|
|
193
|
+
const chain = [firstBlock];
|
|
194
|
+
let lastIndex = startIndex;
|
|
195
|
+
while (blocks[lastIndex].keepWithNext) {
|
|
196
|
+
const nextBlock = blocks[lastIndex + 1];
|
|
197
|
+
if (!nextBlock || nextBlock.isPageBreak || nextBlock.pageBreakBefore) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
chain.push(nextBlock);
|
|
201
|
+
lastIndex++;
|
|
202
|
+
}
|
|
203
|
+
return chain;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Measures the visible body height of a keep-with-next chain using the same
|
|
207
|
+
* collapsed-margin rules as normal block placement. The trailing margin is
|
|
208
|
+
* intentionally excluded, matching the individual block fit check.
|
|
209
|
+
*/
|
|
210
|
+
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
|
|
211
|
+
const firstBlock = chain[0];
|
|
212
|
+
if (!firstBlock) return 0;
|
|
213
|
+
const firstMarginTop = isFirstOnPage ? firstBlock.marginTopPt : Math.max(firstBlock.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
214
|
+
let bodyHeight = firstMarginTop + firstBlock.heightPt;
|
|
215
|
+
for (let index = 1; index < chain.length; index++) {
|
|
216
|
+
const previousBlock = chain[index - 1];
|
|
217
|
+
const block = chain[index];
|
|
218
|
+
bodyHeight += Math.max(previousBlock.marginBottomPt, block.marginTopPt) + block.heightPt;
|
|
219
|
+
}
|
|
220
|
+
return bodyHeight;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Finds footnote references introduced by a sequence of blocks, preserving
|
|
224
|
+
* document order and excluding references already assigned to the page.
|
|
225
|
+
*/
|
|
226
|
+
collectNewFootnoteIds(blocks, existingFootnoteIds) {
|
|
227
|
+
const knownIds = new Set(existingFootnoteIds);
|
|
228
|
+
const newIds = [];
|
|
229
|
+
for (const block of blocks) {
|
|
230
|
+
for (const id of this.extractFootnoteRefs(block.element)) {
|
|
231
|
+
if (!knownIds.has(id)) {
|
|
232
|
+
knownIds.add(id);
|
|
233
|
+
newIds.push(id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return newIds;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The shortest body available to a section's first, default, or even page.
|
|
241
|
+
* A row fragment must fit every variant, otherwise a later header/footer could
|
|
242
|
+
* send it through the oversized-block fallback again.
|
|
243
|
+
*/
|
|
244
|
+
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
245
|
+
return Math.min(
|
|
246
|
+
this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
|
|
247
|
+
this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
|
|
248
|
+
this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Builds a clone of a simple table wrapper containing a contiguous run of rows.
|
|
253
|
+
* Complex table features are deliberately rejected by the caller: a split across
|
|
254
|
+
* merged cells, nested tables, or footnotes cannot be made correct by cloning rows.
|
|
255
|
+
*/
|
|
256
|
+
createSimpleTableFragment(wrapper, table, body, rows, retainAnchor) {
|
|
257
|
+
const wrapperClone = wrapper.cloneNode(false);
|
|
258
|
+
const tableClone = table.cloneNode(false);
|
|
259
|
+
for (const child of Array.from(table.children)) {
|
|
260
|
+
if (child !== body) {
|
|
261
|
+
tableClone.appendChild(child.cloneNode(true));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const bodyClone = body.cloneNode(false);
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
bodyClone.appendChild(row.cloneNode(true));
|
|
267
|
+
}
|
|
268
|
+
tableClone.appendChild(bodyClone);
|
|
269
|
+
if (!retainAnchor) {
|
|
270
|
+
wrapperClone.removeAttribute("data-anchor");
|
|
271
|
+
tableClone.removeAttribute("data-anchor");
|
|
272
|
+
}
|
|
273
|
+
wrapperClone.appendChild(tableClone);
|
|
274
|
+
return wrapperClone;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Splits an oversized, ordinary table at row boundaries. This only participates
|
|
278
|
+
* in the existing oversized-block fallback; unsupported tables keep the previous
|
|
279
|
+
* overflow behavior rather than risking broken table semantics.
|
|
280
|
+
*/
|
|
281
|
+
trySplitSimpleOversizedTable(block, dims, sectionIndex) {
|
|
282
|
+
const wrapper = block.element;
|
|
283
|
+
if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const table = wrapper.firstElementChild;
|
|
287
|
+
if (!(table instanceof HTMLTableElement)) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
291
|
+
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]")) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const rows = Array.from(body.rows);
|
|
295
|
+
const minimumContentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
296
|
+
const maximumFragmentHeight = minimumContentHeight - block.marginTopPt - block.marginBottomPt;
|
|
297
|
+
if (maximumFragmentHeight <= 0) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const groups = [];
|
|
301
|
+
let start = 0;
|
|
302
|
+
while (start < rows.length) {
|
|
303
|
+
let end = start;
|
|
304
|
+
while (end < rows.length) {
|
|
305
|
+
const candidate = this.createSimpleTableFragment(
|
|
306
|
+
wrapper,
|
|
307
|
+
table,
|
|
308
|
+
body,
|
|
309
|
+
rows.slice(start, end + 1),
|
|
310
|
+
start === 0
|
|
311
|
+
);
|
|
312
|
+
const measured = this.measureElement(candidate, dims);
|
|
313
|
+
if (measured.heightPt > maximumFragmentHeight) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
end++;
|
|
317
|
+
}
|
|
318
|
+
if (end === start) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
groups.push(rows.slice(start, end));
|
|
322
|
+
start = end;
|
|
323
|
+
}
|
|
324
|
+
if (groups.length < 2) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const fragments = [];
|
|
328
|
+
for (let index = 0; index < groups.length; index++) {
|
|
329
|
+
const isFirst = index === 0;
|
|
330
|
+
const isLast = index === groups.length - 1;
|
|
331
|
+
const fragment = this.createSimpleTableFragment(
|
|
332
|
+
wrapper,
|
|
333
|
+
table,
|
|
334
|
+
body,
|
|
335
|
+
groups[index],
|
|
336
|
+
isFirst
|
|
337
|
+
);
|
|
338
|
+
if (!isFirst) {
|
|
339
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
340
|
+
}
|
|
341
|
+
if (!isLast) {
|
|
342
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
343
|
+
}
|
|
344
|
+
const measured = this.measureElement(fragment, dims);
|
|
345
|
+
if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
fragments.push({
|
|
349
|
+
...measured,
|
|
350
|
+
keepWithNext: false,
|
|
351
|
+
keepLines: false,
|
|
352
|
+
pageBreakBefore: false,
|
|
353
|
+
isPageBreak: false
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return fragments;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A DOM endpoint that can finish a paragraph fragment. Endpoints are chosen
|
|
360
|
+
* after whitespace or at a run boundary so the paginator never deliberately
|
|
361
|
+
* cuts through an ordinary word merely to fill a little more of a page.
|
|
362
|
+
*/
|
|
363
|
+
paragraphFragmentEndpoints(paragraph) {
|
|
364
|
+
const endpoints = [];
|
|
365
|
+
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
|
|
366
|
+
let textNode;
|
|
367
|
+
while (textNode = walker.nextNode()) {
|
|
368
|
+
const text = textNode.data;
|
|
369
|
+
if (text.length === 0) continue;
|
|
370
|
+
const whitespace = /\s+/g;
|
|
371
|
+
let match;
|
|
372
|
+
while ((match = whitespace.exec(text)) !== null) {
|
|
373
|
+
endpoints.push({ node: textNode, offset: match.index + match[0].length });
|
|
374
|
+
}
|
|
375
|
+
if (endpoints.length === 0 || endpoints[endpoints.length - 1].node !== textNode || endpoints[endpoints.length - 1].offset !== text.length) {
|
|
376
|
+
endpoints.push({ node: textNode, offset: text.length });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return endpoints;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Whether a range contains visible text after ignoring bidi/zero-width marks.
|
|
383
|
+
* Paragraph fragmentation deliberately excludes non-textual descendants, so
|
|
384
|
+
* this is enough to reject empty head or tail fragments.
|
|
385
|
+
*/
|
|
386
|
+
hasVisibleFragmentText(fragment) {
|
|
387
|
+
return (fragment.textContent || "").replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Phase one only handles text-like paragraph descendants. Objects, explicit
|
|
391
|
+
* line breaks, list markers, notes, and out-of-flow/inline-block content all
|
|
392
|
+
* require their own line-layout rules and retain the established whole-block
|
|
393
|
+
* fallback instead of risking broken content or duplicate anchors.
|
|
394
|
+
*/
|
|
395
|
+
canFragmentParagraph(block) {
|
|
396
|
+
if (!this.fragmentParagraphs || block.element.tagName !== "P") {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
const paragraph = block.element;
|
|
400
|
+
if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const unsupportedDescendants = [
|
|
404
|
+
"br",
|
|
405
|
+
"img",
|
|
406
|
+
"picture",
|
|
407
|
+
"svg",
|
|
408
|
+
"math",
|
|
409
|
+
"canvas",
|
|
410
|
+
"video",
|
|
411
|
+
"audio",
|
|
412
|
+
"iframe",
|
|
413
|
+
"object",
|
|
414
|
+
"embed",
|
|
415
|
+
"input",
|
|
416
|
+
"button",
|
|
417
|
+
"select",
|
|
418
|
+
"textarea",
|
|
419
|
+
"table",
|
|
420
|
+
"ol",
|
|
421
|
+
"ul",
|
|
422
|
+
"li",
|
|
423
|
+
"dl",
|
|
424
|
+
"div",
|
|
425
|
+
"p",
|
|
426
|
+
"section",
|
|
427
|
+
"article",
|
|
428
|
+
"aside",
|
|
429
|
+
"figure",
|
|
430
|
+
"fieldset",
|
|
431
|
+
"details",
|
|
432
|
+
"[data-footnote-id]",
|
|
433
|
+
"[data-list-marker]",
|
|
434
|
+
"[data-anchor]",
|
|
435
|
+
"[id]",
|
|
436
|
+
"[contenteditable]"
|
|
437
|
+
].join(", ");
|
|
438
|
+
if (paragraph.querySelector(unsupportedDescendants)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
const paragraphStyle = window.getComputedStyle(paragraph);
|
|
442
|
+
if (paragraphStyle.display !== "block" || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
|
|
446
|
+
const style = window.getComputedStyle(descendant);
|
|
447
|
+
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Builds one range-cloned paragraph fragment. Only the leading fragment keeps
|
|
455
|
+
* the source paragraph's addressability; continuations must not duplicate an
|
|
456
|
+
* id/data-anchor in the rendered document.
|
|
457
|
+
*/
|
|
458
|
+
createParagraphFragment(paragraph, range, retainSourceIdentity, isFinalFragment) {
|
|
459
|
+
const fragment = paragraph.cloneNode(false);
|
|
460
|
+
const contents = range.cloneContents();
|
|
461
|
+
fragment.appendChild(contents);
|
|
462
|
+
if (!retainSourceIdentity) {
|
|
463
|
+
fragment.removeAttribute("id");
|
|
464
|
+
fragment.removeAttribute("data-anchor");
|
|
465
|
+
fragment.style.setProperty("margin-top", "0", "important");
|
|
466
|
+
fragment.style.setProperty("text-indent", "0", "important");
|
|
467
|
+
}
|
|
468
|
+
if (!isFinalFragment) {
|
|
469
|
+
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
470
|
+
}
|
|
471
|
+
return fragment;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Splits a simple paragraph at the largest DOM Range endpoint that fits the
|
|
475
|
+
* currently available body space. The caller then processes the tail normally,
|
|
476
|
+
* allowing it to fragment again on later pages when necessary.
|
|
477
|
+
*/
|
|
478
|
+
tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
|
|
479
|
+
if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
const paragraph = block.element;
|
|
483
|
+
const endpoints = this.paragraphFragmentEndpoints(paragraph);
|
|
484
|
+
if (endpoints.length < 2) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
let low = 0;
|
|
488
|
+
let high = endpoints.length - 2;
|
|
489
|
+
let best = null;
|
|
490
|
+
while (low <= high) {
|
|
491
|
+
const middle = Math.floor((low + high) / 2);
|
|
492
|
+
const endpoint = endpoints[middle];
|
|
493
|
+
const headRange = document.createRange();
|
|
494
|
+
headRange.setStart(paragraph, 0);
|
|
495
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
496
|
+
const headContents = headRange.cloneContents();
|
|
497
|
+
if (!this.hasVisibleFragmentText(headContents)) {
|
|
498
|
+
low = middle + 1;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const head = this.createParagraphFragment(paragraph, headRange, true, false);
|
|
502
|
+
const measured = this.measureElement(head, dims);
|
|
503
|
+
if (effectiveMarginTopPt + measured.heightPt <= availableHeightPt) {
|
|
504
|
+
best = { endpoint, element: head, measured };
|
|
505
|
+
low = middle + 1;
|
|
506
|
+
} else {
|
|
507
|
+
high = middle - 1;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!best) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const tailRange = document.createRange();
|
|
514
|
+
tailRange.setStart(best.endpoint.node, best.endpoint.offset);
|
|
515
|
+
tailRange.setEnd(paragraph, paragraph.childNodes.length);
|
|
516
|
+
const tailContents = tailRange.cloneContents();
|
|
517
|
+
if (!this.hasVisibleFragmentText(tailContents)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
|
|
521
|
+
const tailMeasured = this.measureElement(tail, dims);
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
...best.measured,
|
|
525
|
+
element: best.element,
|
|
526
|
+
keepWithNext: false,
|
|
527
|
+
keepLines: false,
|
|
528
|
+
pageBreakBefore: false,
|
|
529
|
+
isPageBreak: false
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
...tailMeasured,
|
|
533
|
+
element: tail,
|
|
534
|
+
keepWithNext: false,
|
|
535
|
+
keepLines: false,
|
|
536
|
+
pageBreakBefore: false,
|
|
537
|
+
isPageBreak: false
|
|
538
|
+
}
|
|
539
|
+
];
|
|
540
|
+
}
|
|
153
541
|
/**
|
|
154
542
|
* Parses the header/footer registry from the staging element.
|
|
155
543
|
* Also measures heights during parsing for lazy-loading compatibility.
|
|
@@ -548,7 +936,8 @@ var DocxodusEditor = (() => {
|
|
|
548
936
|
currentFootnoteHeight = this.measureContinuationHeight(currentContinuation, dims.contentWidth);
|
|
549
937
|
}
|
|
550
938
|
const finishPage = () => {
|
|
551
|
-
|
|
939
|
+
const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
940
|
+
if (currentContent.length === 0 && !hasCurrentContinuation) return;
|
|
552
941
|
const page = this.createPage(
|
|
553
942
|
dims,
|
|
554
943
|
pageNumber,
|
|
@@ -580,7 +969,6 @@ var DocxodusEditor = (() => {
|
|
|
580
969
|
};
|
|
581
970
|
for (let i = 0; i < blocks.length; i++) {
|
|
582
971
|
const block = blocks[i];
|
|
583
|
-
const nextBlock = blocks[i + 1];
|
|
584
972
|
if (block.isPageBreak) {
|
|
585
973
|
finishPage();
|
|
586
974
|
continue;
|
|
@@ -588,8 +976,59 @@ var DocxodusEditor = (() => {
|
|
|
588
976
|
if (block.pageBreakBefore && currentContent.length > 0) {
|
|
589
977
|
finishPage();
|
|
590
978
|
}
|
|
591
|
-
const
|
|
592
|
-
const
|
|
979
|
+
const previousBlock = blocks[i - 1];
|
|
980
|
+
const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
|
|
981
|
+
const pageHasOccupiedSpace = currentContent.length > 0 || currentFootnoteHeight > 0;
|
|
982
|
+
if (pageHasOccupiedSpace && block.keepWithNext && startsKeepChain) {
|
|
983
|
+
const keepChain = this.getKeepWithNextChain(blocks, i);
|
|
984
|
+
if (keepChain.length > 1) {
|
|
985
|
+
const newChainFootnoteIds = this.collectNewFootnoteIds(
|
|
986
|
+
keepChain,
|
|
987
|
+
currentFootnoteIds
|
|
988
|
+
);
|
|
989
|
+
let additionalChainFootnoteHeight = 0;
|
|
990
|
+
if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
991
|
+
const totalChainFootnoteHeight = this.measureFootnotesHeight(
|
|
992
|
+
[...currentFootnoteIds, ...newChainFootnoteIds],
|
|
993
|
+
dims.contentWidth,
|
|
994
|
+
currentContinuation
|
|
995
|
+
);
|
|
996
|
+
additionalChainFootnoteHeight = Math.max(
|
|
997
|
+
0,
|
|
998
|
+
totalChainFootnoteHeight - currentFootnoteHeight
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1002
|
+
keepChain,
|
|
1003
|
+
prevMarginBottomPt,
|
|
1004
|
+
currentContent.length === 0
|
|
1005
|
+
) + additionalChainFootnoteHeight;
|
|
1006
|
+
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1007
|
+
if (currentChainHeight > currentAvailableHeight) {
|
|
1008
|
+
const nextPageHeights = this.getEffectiveHeights(
|
|
1009
|
+
dims,
|
|
1010
|
+
sectionIndex,
|
|
1011
|
+
pageInSection + 1,
|
|
1012
|
+
pageNumber + 1
|
|
1013
|
+
);
|
|
1014
|
+
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1015
|
+
keepChain,
|
|
1016
|
+
0,
|
|
1017
|
+
true
|
|
1018
|
+
);
|
|
1019
|
+
const freshChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1020
|
+
newChainFootnoteIds,
|
|
1021
|
+
dims.contentWidth,
|
|
1022
|
+
nextPageContinuation
|
|
1023
|
+
);
|
|
1024
|
+
if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
|
|
1025
|
+
finishPage();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
1031
|
+
const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
|
|
593
1032
|
let additionalFootnoteHeight = 0;
|
|
594
1033
|
if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
595
1034
|
const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
|
|
@@ -606,15 +1045,23 @@ var DocxodusEditor = (() => {
|
|
|
606
1045
|
effectiveMarginTop = Math.max(block.marginTopPt, prevMarginBottomPt) - prevMarginBottomPt;
|
|
607
1046
|
}
|
|
608
1047
|
const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
|
|
609
|
-
let neededHeight = blockSpace;
|
|
610
|
-
if (block.keepWithNext && nextBlock && !nextBlock.isPageBreak) {
|
|
611
|
-
const collapsedMargin = Math.max(block.marginBottomPt, nextBlock.marginTopPt);
|
|
612
|
-
neededHeight = effectiveMarginTop + block.heightPt + collapsedMargin + nextBlock.heightPt + additionalFootnoteHeight;
|
|
613
|
-
}
|
|
614
1048
|
const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
|
|
615
1049
|
const bodyContentUsed = effectiveContentHeight - remainingHeight;
|
|
616
1050
|
const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
617
1051
|
const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
|
|
1052
|
+
if (blockSpace > effectiveRemainingHeight) {
|
|
1053
|
+
const paragraphFragments = this.tryFragmentParagraph(
|
|
1054
|
+
block,
|
|
1055
|
+
dims,
|
|
1056
|
+
effectiveRemainingHeight,
|
|
1057
|
+
effectiveMarginTop
|
|
1058
|
+
);
|
|
1059
|
+
if (paragraphFragments) {
|
|
1060
|
+
blocks.splice(i, 1, ...paragraphFragments);
|
|
1061
|
+
i--;
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
618
1065
|
if (blockSpace <= effectiveRemainingHeight) {
|
|
619
1066
|
currentContent.push(block.element.cloneNode(true));
|
|
620
1067
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
@@ -656,10 +1103,12 @@ var DocxodusEditor = (() => {
|
|
|
656
1103
|
footnoteId,
|
|
657
1104
|
fittingElements: fits
|
|
658
1105
|
});
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1106
|
+
if (overflow.length > 0) {
|
|
1107
|
+
nextPageContinuation = {
|
|
1108
|
+
footnoteId,
|
|
1109
|
+
remainingElements: overflow
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
663
1112
|
currentFootnoteHeight = availableForFootnotes;
|
|
664
1113
|
} else {
|
|
665
1114
|
nextPageContinuation = {
|
|
@@ -692,30 +1141,39 @@ var DocxodusEditor = (() => {
|
|
|
692
1141
|
currentFootnoteHeight = expandedFootnoteSpace;
|
|
693
1142
|
} else {
|
|
694
1143
|
finishPage();
|
|
695
|
-
const newPageFootnoteHeight =
|
|
1144
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
696
1145
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
697
1146
|
currentContent.push(block.element.cloneNode(true));
|
|
698
1147
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
699
1148
|
prevMarginBottomPt = block.marginBottomPt;
|
|
700
|
-
currentFootnoteIds = [...
|
|
1149
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
701
1150
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
702
1151
|
}
|
|
703
1152
|
} else {
|
|
704
1153
|
finishPage();
|
|
705
|
-
const newPageFootnoteHeight =
|
|
1154
|
+
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
706
1155
|
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
707
1156
|
currentContent.push(block.element.cloneNode(true));
|
|
708
1157
|
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
709
1158
|
prevMarginBottomPt = block.marginBottomPt;
|
|
710
|
-
currentFootnoteIds = [...
|
|
1159
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
711
1160
|
currentFootnoteHeight = newPageFootnoteHeight;
|
|
712
1161
|
}
|
|
713
1162
|
} else {
|
|
1163
|
+
const tableFragments = this.trySplitSimpleOversizedTable(block, dims, sectionIndex);
|
|
1164
|
+
if (tableFragments) {
|
|
1165
|
+
if (currentContent.length > 0 || currentContinuation) {
|
|
1166
|
+
finishPage();
|
|
1167
|
+
}
|
|
1168
|
+
blocks.splice(i, 1, ...tableFragments);
|
|
1169
|
+
i--;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
714
1172
|
if (currentContent.length > 0) {
|
|
715
1173
|
finishPage();
|
|
716
1174
|
}
|
|
717
1175
|
currentContent.push(block.element.cloneNode(true));
|
|
718
|
-
currentFootnoteIds = [...
|
|
1176
|
+
currentFootnoteIds = [...allBlockFootnoteIds];
|
|
719
1177
|
finishPage();
|
|
720
1178
|
}
|
|
721
1179
|
}
|
|
@@ -839,10 +1297,360 @@ var DocxodusEditor = (() => {
|
|
|
839
1297
|
if (!pageContainer) {
|
|
840
1298
|
throw new Error("Pagination container element not found");
|
|
841
1299
|
}
|
|
842
|
-
const engine = new PaginationEngine(staging, pageContainer,
|
|
1300
|
+
const engine = new PaginationEngine(staging, pageContainer, {
|
|
1301
|
+
...options,
|
|
1302
|
+
fragmentParagraphs: options.fragmentParagraphs ?? true
|
|
1303
|
+
});
|
|
843
1304
|
return engine.paginate();
|
|
844
1305
|
}
|
|
845
1306
|
|
|
1307
|
+
// src/editor-headerfooter.ts
|
|
1308
|
+
var KIND_LABELS = [
|
|
1309
|
+
{ value: "default", label: "Default" },
|
|
1310
|
+
{ value: "first", label: "First page" },
|
|
1311
|
+
{ value: "even", label: "Even pages" }
|
|
1312
|
+
];
|
|
1313
|
+
var EVEN_WARNING = "w:evenAndOddHeaders is document-global and governs footers too \u2014 even pages stop inheriting the Default stories, so a section with only a Default footer shows no footer at all on even pages.";
|
|
1314
|
+
var FIRST_WARNING = "w:titlePg makes page 1 use its own first-page header AND footer instead of the Default ones, so an empty first-page footer leaves page 1 with no footer.";
|
|
1315
|
+
var STORY_BLOCK_KINDS = /* @__PURE__ */ new Set(["p", "h", "li"]);
|
|
1316
|
+
var HeaderFooterRegion = class {
|
|
1317
|
+
constructor(bridge, handle, options, callbacks) {
|
|
1318
|
+
/** The body anchor whose section the bands currently describe (needed to seed a story). */
|
|
1319
|
+
this.bodyAnchorId = null;
|
|
1320
|
+
this.sectionInfo = null;
|
|
1321
|
+
/** Memoized body anchor → governing sectionUnid, so repeat focus costs no bridge call. */
|
|
1322
|
+
this.sectionOfAnchor = /* @__PURE__ */ new Map();
|
|
1323
|
+
this.kinds = {
|
|
1324
|
+
header: "default",
|
|
1325
|
+
footer: "default"
|
|
1326
|
+
};
|
|
1327
|
+
this.bridge = bridge;
|
|
1328
|
+
this.handle = handle;
|
|
1329
|
+
this.options = options;
|
|
1330
|
+
this.callbacks = callbacks;
|
|
1331
|
+
this.headerBand = this.buildBand("header");
|
|
1332
|
+
this.footerBand = this.buildBand("footer");
|
|
1333
|
+
}
|
|
1334
|
+
// ─── public surface ──────────────────────────────────────────────────
|
|
1335
|
+
/**
|
|
1336
|
+
* Point the bands at the section governing `bodyAnchorId` and repaint if it changed.
|
|
1337
|
+
* Cheap to call on every focus change: the anchor → section lookup is memoized, and an
|
|
1338
|
+
* unchanged `sectionUnid` returns without touching the DOM (so it never clobbers the
|
|
1339
|
+
* user's kind selection).
|
|
1340
|
+
*/
|
|
1341
|
+
syncToBody(bodyAnchorId) {
|
|
1342
|
+
if (!bodyAnchorId) return;
|
|
1343
|
+
const cached = this.sectionOfAnchor.get(bodyAnchorId);
|
|
1344
|
+
if (cached !== void 0 && cached === this.sectionInfo?.sectionUnid) {
|
|
1345
|
+
this.bodyAnchorId = bodyAnchorId;
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const info = this.readSectionInfo(bodyAnchorId);
|
|
1349
|
+
if (!info) return;
|
|
1350
|
+
this.sectionOfAnchor.set(bodyAnchorId, info.sectionUnid);
|
|
1351
|
+
const changed = info.sectionUnid !== this.sectionInfo?.sectionUnid;
|
|
1352
|
+
this.bodyAnchorId = bodyAnchorId;
|
|
1353
|
+
this.sectionInfo = info;
|
|
1354
|
+
if (changed) this.refreshAll();
|
|
1355
|
+
}
|
|
1356
|
+
/** Repaint one band from the live session. */
|
|
1357
|
+
refresh(which) {
|
|
1358
|
+
this.sectionInfo = this.bodyAnchorId ? this.readSectionInfo(this.bodyAnchorId) : null;
|
|
1359
|
+
this.renderBand(which);
|
|
1360
|
+
}
|
|
1361
|
+
/** Repaint both bands from the live session (after a remount, undo/redo, or section change). */
|
|
1362
|
+
refreshAll() {
|
|
1363
|
+
this.renderBand("header");
|
|
1364
|
+
this.renderBand("footer");
|
|
1365
|
+
}
|
|
1366
|
+
/** Select (and, if absent, create) the story kind a band edits, and make it render. */
|
|
1367
|
+
setKind(which, kind) {
|
|
1368
|
+
this.kinds[which] = kind;
|
|
1369
|
+
const select = this.bandFor(which).querySelector("[data-hf-kind]");
|
|
1370
|
+
if (select && select.value !== kind) select.value = kind;
|
|
1371
|
+
if (!this.partUriFor(which, kind)) this.seedStory(which, kind);
|
|
1372
|
+
if (this.bodyAnchorId && kind !== "default") {
|
|
1373
|
+
this.bridge.EnsureHeaderFooterVisible(this.handle, this.bodyAnchorId, kind);
|
|
1374
|
+
}
|
|
1375
|
+
this.refresh(which);
|
|
1376
|
+
}
|
|
1377
|
+
/** The kind a band is currently editing. */
|
|
1378
|
+
kindOf(which) {
|
|
1379
|
+
return this.kinds[which];
|
|
1380
|
+
}
|
|
1381
|
+
/** Append a PAGE / NUMPAGES field to the story paragraph addressed by `anchorId`. */
|
|
1382
|
+
insertPageNumber(which, anchorId, field) {
|
|
1383
|
+
const res = parseResult(this.bridge.InsertPageNumberField(this.handle, anchorId, field));
|
|
1384
|
+
if (!res.success) return false;
|
|
1385
|
+
this.refresh(which);
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
/** Insert a page number into a band's own target paragraph (focused, else last). Seeds the
|
|
1389
|
+
* story first if the band's selected kind has none, so the command is never a silent no-op. */
|
|
1390
|
+
insertPageNumberInBand(which, field) {
|
|
1391
|
+
if (!this.partUriFor(which, this.kinds[which])) {
|
|
1392
|
+
this.seedStory(which, this.kinds[which]);
|
|
1393
|
+
this.renderBand(which);
|
|
1394
|
+
}
|
|
1395
|
+
const target = this.pageNumberTarget(which);
|
|
1396
|
+
return target ? this.insertPageNumber(which, target, field) : false;
|
|
1397
|
+
}
|
|
1398
|
+
/** The band element containing `node`, or null. */
|
|
1399
|
+
bandOf(node) {
|
|
1400
|
+
const el = node && node.nodeType === 1 ? node : node?.parentElement ?? null;
|
|
1401
|
+
const band = el?.closest("[data-hf-band]") ?? null;
|
|
1402
|
+
return band === this.headerBand || band === this.footerBand ? band : null;
|
|
1403
|
+
}
|
|
1404
|
+
/** The block-list root (a band's story container) owning `node`, or null. */
|
|
1405
|
+
blockRootOf(node) {
|
|
1406
|
+
const band = this.bandOf(node);
|
|
1407
|
+
return band ? band.querySelector("[data-hf-body]") : null;
|
|
1408
|
+
}
|
|
1409
|
+
/** True when `node` is inside either band (including its chrome). */
|
|
1410
|
+
contains(node) {
|
|
1411
|
+
return this.bandOf(node) !== null;
|
|
1412
|
+
}
|
|
1413
|
+
/** `"header"` / `"footer"` for a band element produced by this region. */
|
|
1414
|
+
whichOf(band) {
|
|
1415
|
+
return band === this.footerBand ? "footer" : "header";
|
|
1416
|
+
}
|
|
1417
|
+
// ─── section + anchor resolution ─────────────────────────────────────
|
|
1418
|
+
readSectionInfo(bodyAnchorId) {
|
|
1419
|
+
try {
|
|
1420
|
+
const raw = this.bridge.GetSectionInfo(this.handle, bodyAnchorId);
|
|
1421
|
+
const parsed = JSON.parse(raw);
|
|
1422
|
+
return parsed && typeof parsed.sectionUnid === "string" ? parsed : null;
|
|
1423
|
+
} catch {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
refsFor(which) {
|
|
1428
|
+
const info = this.sectionInfo;
|
|
1429
|
+
if (!info) return [];
|
|
1430
|
+
return (which === "header" ? info.headerRefs : info.footerRefs) ?? [];
|
|
1431
|
+
}
|
|
1432
|
+
/** The reference supplying `kind` for this band — possibly inherited from an earlier section. */
|
|
1433
|
+
refFor(which, kind) {
|
|
1434
|
+
return this.refsFor(which).find((r) => r.kind === kind) ?? null;
|
|
1435
|
+
}
|
|
1436
|
+
/** URI of the part supplying `kind` for this band, or null when the story doesn't exist. */
|
|
1437
|
+
partUriFor(which, kind) {
|
|
1438
|
+
return this.refFor(which, kind)?.partUri ?? null;
|
|
1439
|
+
}
|
|
1440
|
+
/** Full anchor ids of the story paragraphs held in `partUri`, in document order. */
|
|
1441
|
+
storyAnchors(partUri) {
|
|
1442
|
+
let index;
|
|
1443
|
+
try {
|
|
1444
|
+
index = JSON.parse(this.bridge.Project(this.handle)).anchorIndex;
|
|
1445
|
+
} catch {
|
|
1446
|
+
return [];
|
|
1447
|
+
}
|
|
1448
|
+
return Object.entries(index).filter(([, t]) => t.partUri === partUri && STORY_BLOCK_KINDS.has(t.kind)).map(([id]) => id);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Create an empty story for `kind`. Seeding happens the moment a kind is selected rather than
|
|
1452
|
+
* lazily on first keystroke: an absent story renders no contenteditable element, so there
|
|
1453
|
+
* would be nothing to click into.
|
|
1454
|
+
*/
|
|
1455
|
+
seedStory(which, kind) {
|
|
1456
|
+
if (!this.bodyAnchorId) return;
|
|
1457
|
+
const set = which === "header" ? this.bridge.SetHeaderText : this.bridge.SetFooterText;
|
|
1458
|
+
const res = parseResult(set(this.handle, this.bodyAnchorId, kind, ""));
|
|
1459
|
+
if (!res.success) return;
|
|
1460
|
+
this.callbacks.refreshAnchorMap();
|
|
1461
|
+
this.sectionInfo = this.readSectionInfo(this.bodyAnchorId);
|
|
1462
|
+
}
|
|
1463
|
+
// ─── rendering ───────────────────────────────────────────────────────
|
|
1464
|
+
bandFor(which) {
|
|
1465
|
+
return which === "header" ? this.headerBand : this.footerBand;
|
|
1466
|
+
}
|
|
1467
|
+
buildBand(which) {
|
|
1468
|
+
const band = document.createElement("div");
|
|
1469
|
+
band.className = "docx-hf-band";
|
|
1470
|
+
band.setAttribute("data-hf-band", which);
|
|
1471
|
+
const chrome = document.createElement("div");
|
|
1472
|
+
chrome.className = "docx-hf-chrome";
|
|
1473
|
+
const label = document.createElement("span");
|
|
1474
|
+
label.className = "docx-hf-label";
|
|
1475
|
+
label.textContent = which === "header" ? "Header" : "Footer";
|
|
1476
|
+
chrome.appendChild(label);
|
|
1477
|
+
const kindSelect = document.createElement("select");
|
|
1478
|
+
kindSelect.setAttribute("data-hf-kind", "");
|
|
1479
|
+
kindSelect.title = "Which header/footer story to edit";
|
|
1480
|
+
for (const k of KIND_LABELS) {
|
|
1481
|
+
const opt = document.createElement("option");
|
|
1482
|
+
opt.value = k.value;
|
|
1483
|
+
opt.textContent = k.label;
|
|
1484
|
+
kindSelect.appendChild(opt);
|
|
1485
|
+
}
|
|
1486
|
+
kindSelect.addEventListener(
|
|
1487
|
+
"change",
|
|
1488
|
+
() => this.setKind(which, kindSelect.value)
|
|
1489
|
+
);
|
|
1490
|
+
chrome.appendChild(kindSelect);
|
|
1491
|
+
const pageNum = document.createElement("select");
|
|
1492
|
+
pageNum.setAttribute("data-hf-pagenum", "");
|
|
1493
|
+
pageNum.title = "Insert a page-number field into the focused story paragraph";
|
|
1494
|
+
for (const o of [
|
|
1495
|
+
{ value: "", label: "Page \u2116\u2026" },
|
|
1496
|
+
{ value: "currentPage", label: "Page number" },
|
|
1497
|
+
{ value: "totalPages", label: "Total pages" }
|
|
1498
|
+
]) {
|
|
1499
|
+
const opt = document.createElement("option");
|
|
1500
|
+
opt.value = o.value;
|
|
1501
|
+
opt.textContent = o.label;
|
|
1502
|
+
pageNum.appendChild(opt);
|
|
1503
|
+
}
|
|
1504
|
+
pageNum.addEventListener("change", () => {
|
|
1505
|
+
const field = pageNum.value;
|
|
1506
|
+
pageNum.value = "";
|
|
1507
|
+
if (field !== "currentPage" && field !== "totalPages") return;
|
|
1508
|
+
const target = this.pageNumberTarget(which);
|
|
1509
|
+
if (target) this.insertPageNumber(which, target, field);
|
|
1510
|
+
});
|
|
1511
|
+
chrome.appendChild(pageNum);
|
|
1512
|
+
const inheritedNote = document.createElement("span");
|
|
1513
|
+
inheritedNote.className = "docx-hf-inherited";
|
|
1514
|
+
inheritedNote.setAttribute("data-hf-inherited-note", "");
|
|
1515
|
+
inheritedNote.hidden = true;
|
|
1516
|
+
chrome.appendChild(inheritedNote);
|
|
1517
|
+
band.appendChild(chrome);
|
|
1518
|
+
const warning = document.createElement("div");
|
|
1519
|
+
warning.className = "docx-hf-warning";
|
|
1520
|
+
warning.setAttribute("data-hf-warning", "");
|
|
1521
|
+
warning.hidden = true;
|
|
1522
|
+
band.appendChild(warning);
|
|
1523
|
+
const body = document.createElement("div");
|
|
1524
|
+
body.className = "docx-hf-body";
|
|
1525
|
+
body.setAttribute("data-hf-body", "");
|
|
1526
|
+
band.appendChild(body);
|
|
1527
|
+
return band;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Which story paragraph a page-number insert targets: the last one the user focused in this
|
|
1531
|
+
* band if it is still attached, else the band's last paragraph (Word's convention — the page
|
|
1532
|
+
* number goes at the end of the footer line).
|
|
1533
|
+
*/
|
|
1534
|
+
pageNumberTarget(which) {
|
|
1535
|
+
const body = this.bandFor(which).querySelector("[data-hf-body]");
|
|
1536
|
+
if (!body) return null;
|
|
1537
|
+
const focused = body.querySelector('[data-hf-focused="true"][data-hf-anchor]');
|
|
1538
|
+
const blocks = Array.from(body.querySelectorAll("[data-hf-anchor]"));
|
|
1539
|
+
const target = focused ?? blocks[blocks.length - 1];
|
|
1540
|
+
return target?.getAttribute("data-hf-anchor") ?? null;
|
|
1541
|
+
}
|
|
1542
|
+
renderBand(which) {
|
|
1543
|
+
const band = this.bandFor(which);
|
|
1544
|
+
const body = band.querySelector("[data-hf-body]");
|
|
1545
|
+
if (!body) return;
|
|
1546
|
+
const kind = this.kinds[which];
|
|
1547
|
+
const select = band.querySelector("[data-hf-kind]");
|
|
1548
|
+
if (select && select.value !== kind) select.value = kind;
|
|
1549
|
+
const inherited = this.refFor(which, kind)?.inherited === true;
|
|
1550
|
+
band.toggleAttribute("data-hf-inherited", inherited);
|
|
1551
|
+
const note = band.querySelector("[data-hf-inherited-note]");
|
|
1552
|
+
if (note) {
|
|
1553
|
+
note.hidden = !inherited;
|
|
1554
|
+
note.textContent = inherited ? "inherited from an earlier section" : "";
|
|
1555
|
+
}
|
|
1556
|
+
this.renderKindWarning(which);
|
|
1557
|
+
body.innerHTML = "";
|
|
1558
|
+
const partUri = this.partUriFor(which, kind);
|
|
1559
|
+
if (!partUri) {
|
|
1560
|
+
body.appendChild(this.placeholder(which, kind));
|
|
1561
|
+
band.setAttribute("data-hf-empty", "true");
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
band.removeAttribute("data-hf-empty");
|
|
1565
|
+
const anchors = this.storyAnchors(partUri);
|
|
1566
|
+
if (anchors.length === 0) {
|
|
1567
|
+
body.appendChild(this.placeholder(which, kind));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
for (const anchorId of anchors) {
|
|
1571
|
+
const el = this.renderStoryBlock(anchorId);
|
|
1572
|
+
if (!el) continue;
|
|
1573
|
+
body.appendChild(el);
|
|
1574
|
+
this.adoptBlock(el, anchorId);
|
|
1575
|
+
this.callbacks.wireBlock(el);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Mark a story paragraph as belonging to this region. The full anchor id is stamped alongside
|
|
1580
|
+
* `data-anchor` (which carries only the bare unid, matching the body's convention) so band
|
|
1581
|
+
* chrome can address the block without going through the editor's unid map. Called on first
|
|
1582
|
+
* render AND after an incremental swap, which replaces the DOM node.
|
|
1583
|
+
*/
|
|
1584
|
+
adoptBlock(el, anchorId) {
|
|
1585
|
+
const body = this.blockRootOf(el);
|
|
1586
|
+
if (!body) return;
|
|
1587
|
+
el.setAttribute("data-hf-anchor", anchorId);
|
|
1588
|
+
if (el.dataset.hfAdopted === "true") return;
|
|
1589
|
+
el.dataset.hfAdopted = "true";
|
|
1590
|
+
el.addEventListener("focus", () => {
|
|
1591
|
+
body.querySelectorAll("[data-hf-focused]").forEach((sib) => sib.removeAttribute("data-hf-focused"));
|
|
1592
|
+
el.setAttribute("data-hf-focused", "true");
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
renderStoryBlock(anchorId) {
|
|
1596
|
+
const html = this.bridge.RenderBlockHtml(
|
|
1597
|
+
this.handle,
|
|
1598
|
+
anchorId,
|
|
1599
|
+
this.options.cssPrefix,
|
|
1600
|
+
this.options.fabricateClasses
|
|
1601
|
+
);
|
|
1602
|
+
if (html.charCodeAt(0) === 123) return null;
|
|
1603
|
+
return new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
1604
|
+
}
|
|
1605
|
+
placeholder(which, kind) {
|
|
1606
|
+
const el = document.createElement("div");
|
|
1607
|
+
el.className = "docx-hf-placeholder";
|
|
1608
|
+
el.setAttribute("data-hf-placeholder", "");
|
|
1609
|
+
const label = KIND_LABELS.find((k) => k.value === kind)?.label ?? kind;
|
|
1610
|
+
el.textContent = `No ${label.toLowerCase()} ${which} \u2014 pick a kind to create one.`;
|
|
1611
|
+
return el;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Surface the caveat that comes with the selected kind. Turning on first/even means those pages
|
|
1615
|
+
* stop using the Default stories entirely, which bites hardest on the OTHER band: enabling an
|
|
1616
|
+
* even header with no even footer leaves even pages footer-less, and enabling a first-page
|
|
1617
|
+
* header with an empty first-page footer leaves page 1 footer-less. The note is shown whenever
|
|
1618
|
+
* first/even is selected (the behavior change is real either way); the fix button appears only
|
|
1619
|
+
* when the counterpart story is missing entirely, which is what a user almost always wants next.
|
|
1620
|
+
*/
|
|
1621
|
+
renderKindWarning(which) {
|
|
1622
|
+
const band = this.bandFor(which);
|
|
1623
|
+
const warning = band.querySelector("[data-hf-warning]");
|
|
1624
|
+
if (!warning) return;
|
|
1625
|
+
const kind = this.kinds[which];
|
|
1626
|
+
if (kind === "default") {
|
|
1627
|
+
warning.hidden = true;
|
|
1628
|
+
warning.textContent = "";
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const other = which === "header" ? "footer" : "header";
|
|
1632
|
+
warning.hidden = false;
|
|
1633
|
+
warning.textContent = `${kind === "even" ? EVEN_WARNING : FIRST_WARNING} `;
|
|
1634
|
+
if (this.partUriFor(other, kind)) return;
|
|
1635
|
+
const fix = document.createElement("button");
|
|
1636
|
+
fix.type = "button";
|
|
1637
|
+
fix.setAttribute("data-hf-fix-counterpart", "");
|
|
1638
|
+
fix.textContent = `Also create a ${kind === "even" ? "matching even" : "first-page"} ${other}`;
|
|
1639
|
+
fix.addEventListener("click", () => {
|
|
1640
|
+
this.setKind(other, kind);
|
|
1641
|
+
this.renderKindWarning(which);
|
|
1642
|
+
});
|
|
1643
|
+
warning.appendChild(fix);
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
function parseResult(json) {
|
|
1647
|
+
try {
|
|
1648
|
+
return JSON.parse(json);
|
|
1649
|
+
} catch {
|
|
1650
|
+
return { success: false };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
846
1654
|
// src/editor.ts
|
|
847
1655
|
var EDITABLE_TAGS = /* @__PURE__ */ new Set(["P", "H1", "H2", "H3", "H4", "H5", "H6"]);
|
|
848
1656
|
function fontWeightIsBold(w) {
|
|
@@ -976,11 +1784,22 @@ var DocxodusEditor = (() => {
|
|
|
976
1784
|
return out;
|
|
977
1785
|
}
|
|
978
1786
|
function trimmedSplitOffset(block, domOffset) {
|
|
979
|
-
const
|
|
980
|
-
const leading = content.length - content.replace(/^\s+/, "").length;
|
|
981
|
-
const trimmedLen = content.trim().length;
|
|
1787
|
+
const { leading, trimmedLen } = trimBounds(block);
|
|
982
1788
|
return Math.max(0, Math.min(domOffset - Math.min(domOffset, leading), trimmedLen));
|
|
983
1789
|
}
|
|
1790
|
+
function trimBounds(block) {
|
|
1791
|
+
const content = blockContentText(block);
|
|
1792
|
+
return {
|
|
1793
|
+
leading: content.length - content.replace(/^\s+/, "").length,
|
|
1794
|
+
trimmedLen: content.trim().length
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function trimmedSpan(block, span) {
|
|
1798
|
+
const { leading, trimmedLen } = trimBounds(block);
|
|
1799
|
+
const start = Math.max(0, Math.min(span.start - Math.min(span.start, leading), trimmedLen));
|
|
1800
|
+
const end = Math.max(start, Math.min(span.start + span.length - leading, trimmedLen));
|
|
1801
|
+
return { start, length: end - start };
|
|
1802
|
+
}
|
|
984
1803
|
function contentPositionIn(el, offset) {
|
|
985
1804
|
let remaining = offset;
|
|
986
1805
|
let result = null;
|
|
@@ -1047,7 +1866,8 @@ var DocxodusEditor = (() => {
|
|
|
1047
1866
|
if (!block.contains(range.startContainer) || !block.contains(range.endContainer)) return null;
|
|
1048
1867
|
const start = contentOffsetOf(block, range.startContainer, range.startOffset);
|
|
1049
1868
|
const end = contentOffsetOf(block, range.endContainer, range.endOffset);
|
|
1050
|
-
|
|
1869
|
+
const span = trimmedSpan(block, { start: Math.min(start, end), length: Math.abs(end - start) });
|
|
1870
|
+
return span.length > 0 ? span : null;
|
|
1051
1871
|
}
|
|
1052
1872
|
function selectRange(el, start, length) {
|
|
1053
1873
|
const sel = typeof window !== "undefined" ? window.getSelection() : null;
|
|
@@ -1167,6 +1987,8 @@ var DocxodusEditor = (() => {
|
|
|
1167
1987
|
* block, and cleared when a caret is collapsed inside a block (so it never goes stale).
|
|
1168
1988
|
*/
|
|
1169
1989
|
this.lastSelection = null;
|
|
1990
|
+
/** The docked header/footer bands, when `options.headerFooter` is on. */
|
|
1991
|
+
this.region = null;
|
|
1170
1992
|
/** Track the last meaningful selection so focus-stealing toolbar controls can still target it. */
|
|
1171
1993
|
this.onSelectionChange = () => {
|
|
1172
1994
|
if (this.closed) return;
|
|
@@ -1192,12 +2014,39 @@ var DocxodusEditor = (() => {
|
|
|
1192
2014
|
if (typeof document !== "undefined")
|
|
1193
2015
|
document.addEventListener("selectionchange", this.onSelectionChange);
|
|
1194
2016
|
}
|
|
1195
|
-
/** The editable block (contenteditable [data-anchor]) containing `node`, if any, within this editor.
|
|
2017
|
+
/** The editable block (contenteditable [data-anchor]) containing `node`, if any, within this editor.
|
|
2018
|
+
* Fenced by `container`, not `editRoot`, so header/footer band blocks — which live outside the
|
|
2019
|
+
* body edit root by design — also register. The fence still rejects other editors on the page. */
|
|
1196
2020
|
editableBlockOf(node) {
|
|
1197
2021
|
if (!node) return null;
|
|
1198
2022
|
const start = node.nodeType === 1 ? node : node.parentElement;
|
|
1199
2023
|
const block = start?.closest('[data-anchor][contenteditable="true"]') ?? null;
|
|
1200
|
-
return block && this.
|
|
2024
|
+
return block && this.container.contains(block) ? block : null;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* The root owning `el`'s sibling block list: its header/footer band's story container, else the
|
|
2028
|
+
* body edit root. Keeps a multi-block selection from spanning a band and the body, whose block
|
|
2029
|
+
* lists belong to different OOXML parts.
|
|
2030
|
+
*/
|
|
2031
|
+
ownerRoot(el) {
|
|
2032
|
+
return this.region?.blockRootOf(el) ?? this.editRoot;
|
|
2033
|
+
}
|
|
2034
|
+
/** True when `el` is a header/footer band block rather than a body block. */
|
|
2035
|
+
isBandBlock(el) {
|
|
2036
|
+
return !!this.region?.contains(el);
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Repaint after an edit to `block` that would otherwise remount the whole document: a band
|
|
2040
|
+
* repaints only itself (a story is one to three paragraphs), leaving the body DOM — and the
|
|
2041
|
+
* user's place in it — untouched.
|
|
2042
|
+
*/
|
|
2043
|
+
refreshAfter(block, focusIndex, caretAtEnd = false) {
|
|
2044
|
+
const band = this.region?.bandOf(block);
|
|
2045
|
+
if (band) {
|
|
2046
|
+
this.region.refresh(this.region.whichOf(band));
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
this.remount(focusIndex, caretAtEnd);
|
|
1201
2050
|
}
|
|
1202
2051
|
/** Open a document, render it into `container`, and wire up editing. */
|
|
1203
2052
|
static open(container, bytes, exports, options = {}) {
|
|
@@ -1207,16 +2056,19 @@ var DocxodusEditor = (() => {
|
|
|
1207
2056
|
editable: options.editable ?? true,
|
|
1208
2057
|
paginated: options.paginated ?? false,
|
|
1209
2058
|
scale: options.scale ?? 1,
|
|
2059
|
+
headerFooter: options.headerFooter ?? false,
|
|
1210
2060
|
onEdit: options.onEdit
|
|
1211
2061
|
};
|
|
1212
2062
|
const handle = exports.DocxSessionBridge.OpenSession(bytes, '{"persistAnchorIds":true}');
|
|
1213
2063
|
const editor = new _DocxEditor(container, exports, handle, opts);
|
|
1214
2064
|
editor.refreshAnchorMap();
|
|
2065
|
+
if (opts.headerFooter) editor.createRegion();
|
|
1215
2066
|
const fullHtml = exports.DocumentConverter.ConvertDocxToHtmlComplete(
|
|
1216
2067
|
...completeArgs(bytes, opts.cssPrefix, opts.fabricateClasses, opts.paginated, opts.scale)
|
|
1217
2068
|
);
|
|
1218
2069
|
if (opts.paginated) editor.mountPaginated(fullHtml);
|
|
1219
2070
|
else editor.mountHtml(fullHtml);
|
|
2071
|
+
editor.syncRegionToBody();
|
|
1220
2072
|
return editor;
|
|
1221
2073
|
}
|
|
1222
2074
|
/**
|
|
@@ -1259,29 +2111,113 @@ var DocxodusEditor = (() => {
|
|
|
1259
2111
|
assertOpen() {
|
|
1260
2112
|
if (this.closed) throw new Error("DocxEditor is closed");
|
|
1261
2113
|
}
|
|
1262
|
-
/**
|
|
2114
|
+
/**
|
|
2115
|
+
* Rebuild unid → full-anchor-id from the live session projection.
|
|
2116
|
+
*
|
|
2117
|
+
* Unids are CONTENT-ADDRESSED, so blocks with identical content in DIFFERENT parts collide —
|
|
2118
|
+
* e.g. a document with empty default/first/even header stories has one unid for several
|
|
2119
|
+
* header parts. A collision must resolve to the BODY entry: body blocks carry only
|
|
2120
|
+
* `data-anchor` (the bare unid) and have nothing else to resolve through, whereas a
|
|
2121
|
+
* header/footer band block carries its full anchor in `data-hf-anchor` and is resolved from
|
|
2122
|
+
* that (see `anchorIdOf`). Letting a non-body scope win here would silently redirect a body
|
|
2123
|
+
* edit into a header part.
|
|
2124
|
+
*/
|
|
1263
2125
|
refreshAnchorMap() {
|
|
1264
2126
|
const proj = JSON.parse(this.exports.DocxSessionBridge.Project(this.handle));
|
|
1265
2127
|
this.unidToFullId.clear();
|
|
2128
|
+
const bodyOwned = /* @__PURE__ */ new Set();
|
|
1266
2129
|
for (const [fullId, target] of Object.entries(proj.anchorIndex)) {
|
|
2130
|
+
const isBody = target.scope === "body";
|
|
2131
|
+
if (!isBody && bodyOwned.has(target.unid)) continue;
|
|
1267
2132
|
this.unidToFullId.set(target.unid, fullId);
|
|
2133
|
+
if (isBody) bodyOwned.add(target.unid);
|
|
1268
2134
|
}
|
|
1269
2135
|
}
|
|
2136
|
+
/**
|
|
2137
|
+
* The full `kind:scope:unid` anchor for a rendered block. A header/footer band block carries
|
|
2138
|
+
* its own — the unid map cannot disambiguate one, since several parts' story paragraphs can
|
|
2139
|
+
* share a content-addressed unid (a real Word document with empty default/first/even stories
|
|
2140
|
+
* does exactly that, and a unid-keyed lookup would land the edit in the wrong header part).
|
|
2141
|
+
*/
|
|
2142
|
+
anchorIdOf(el) {
|
|
2143
|
+
const stamped = el.getAttribute("data-hf-anchor");
|
|
2144
|
+
if (stamped) return stamped;
|
|
2145
|
+
const unid = el.getAttribute("data-anchor");
|
|
2146
|
+
return unid ? this.unidToFullId.get(unid) : void 0;
|
|
2147
|
+
}
|
|
2148
|
+
/** Build the header/footer region (called once, before the first mount). */
|
|
2149
|
+
createRegion() {
|
|
2150
|
+
this.region = new HeaderFooterRegion(
|
|
2151
|
+
this.exports.DocxSessionBridge,
|
|
2152
|
+
this.handle,
|
|
2153
|
+
{ cssPrefix: this.options.cssPrefix, fabricateClasses: this.options.fabricateClasses },
|
|
2154
|
+
{
|
|
2155
|
+
wireBlock: (el) => this.wireBlock(el),
|
|
2156
|
+
refreshAnchorMap: () => this.refreshAnchorMap()
|
|
2157
|
+
}
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Point the bands at the section governing the current focus (or the first body block).
|
|
2162
|
+
* Called after every mount, and on focus of a body block, so a multi-section document shows
|
|
2163
|
+
* the stories that actually apply where the caret is.
|
|
2164
|
+
*/
|
|
2165
|
+
syncRegionToBody(fromBlock) {
|
|
2166
|
+
if (!this.region) return;
|
|
2167
|
+
const block = fromBlock ?? this.editableList()[0];
|
|
2168
|
+
const unid = block?.getAttribute("data-anchor");
|
|
2169
|
+
const id = unid ? this.unidToFullId.get(unid) : void 0;
|
|
2170
|
+
this.region.syncToBody(id ?? null);
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Insert the bands around `bodyRoot` and make `bodyRoot` the edit root. Band blocks must stay
|
|
2174
|
+
* OUT of the edit root: `editableList()`/`blockIndex()` enumerate it to compute remount focus
|
|
2175
|
+
* indices, and band blocks in that list would shift every index.
|
|
2176
|
+
*/
|
|
2177
|
+
dockBands(bodyRoot) {
|
|
2178
|
+
if (!this.region) return;
|
|
2179
|
+
bodyRoot.before(this.region.headerBand);
|
|
2180
|
+
bodyRoot.after(this.region.footerBand);
|
|
2181
|
+
this.region.refreshAll();
|
|
2182
|
+
}
|
|
1270
2183
|
/** Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks. */
|
|
1271
2184
|
mountHtml(fullHtml) {
|
|
1272
2185
|
const parsed = new DOMParser().parseFromString(fullHtml, "text/html");
|
|
1273
2186
|
const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
|
|
1274
|
-
this.
|
|
1275
|
-
|
|
1276
|
-
|
|
2187
|
+
if (!this.region) {
|
|
2188
|
+
this.container.innerHTML = styles + parsed.body.innerHTML;
|
|
2189
|
+
this.editRoot = this.container;
|
|
2190
|
+
if (this.options.editable) this.wireBlocks(this.container);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
this.container.innerHTML = styles;
|
|
2194
|
+
const flow = document.createElement("div");
|
|
2195
|
+
flow.className = "docx-body-flow";
|
|
2196
|
+
flow.innerHTML = parsed.body.innerHTML;
|
|
2197
|
+
this.container.appendChild(flow);
|
|
2198
|
+
this.editRoot = flow;
|
|
2199
|
+
if (this.options.editable) this.wireBlocks(flow);
|
|
2200
|
+
this.dockBands(flow);
|
|
1277
2201
|
}
|
|
1278
2202
|
/** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */
|
|
1279
2203
|
mountPaginated(fullHtml) {
|
|
1280
|
-
|
|
2204
|
+
let target = this.container;
|
|
2205
|
+
if (this.region) {
|
|
2206
|
+
this.container.innerHTML = "";
|
|
2207
|
+
target = document.createElement("div");
|
|
2208
|
+
target.className = "docx-body-flow";
|
|
2209
|
+
this.container.appendChild(target);
|
|
2210
|
+
}
|
|
2211
|
+
paginateHtml(fullHtml, target, {
|
|
2212
|
+
scale: this.options.scale,
|
|
2213
|
+
cssPrefix: "page-",
|
|
2214
|
+
fragmentParagraphs: false
|
|
2215
|
+
});
|
|
1281
2216
|
this.container.querySelector("#pagination-staging, .page-staging")?.remove();
|
|
1282
|
-
const pageRoot =
|
|
2217
|
+
const pageRoot = target.querySelector("#pagination-container") ?? target;
|
|
1283
2218
|
this.editRoot = pageRoot;
|
|
1284
2219
|
if (this.options.editable) this.wireBlocks(pageRoot);
|
|
2220
|
+
if (this.region) this.dockBands(target);
|
|
1285
2221
|
}
|
|
1286
2222
|
wireBlocks(root) {
|
|
1287
2223
|
root.querySelectorAll("[data-anchor]").forEach((el) => this.wireBlock(el));
|
|
@@ -1289,15 +2225,18 @@ var DocxodusEditor = (() => {
|
|
|
1289
2225
|
wireBlock(el) {
|
|
1290
2226
|
if (!EDITABLE_TAGS.has(el.tagName)) return;
|
|
1291
2227
|
const unid = el.getAttribute("data-anchor");
|
|
1292
|
-
if (!unid || !this.
|
|
2228
|
+
if (!unid || !this.anchorIdOf(el)) return;
|
|
1293
2229
|
el.setAttribute("contenteditable", "true");
|
|
1294
2230
|
el.querySelectorAll("[data-list-marker]").forEach((m) => m.setAttribute("contenteditable", "false"));
|
|
1295
2231
|
el.dataset.committedText = blockContentText(el);
|
|
1296
2232
|
el.addEventListener("focus", () => {
|
|
1297
2233
|
this.activeBlock = el;
|
|
2234
|
+
if (this.region && !this.isBandBlock(el)) this.syncRegionToBody(el);
|
|
1298
2235
|
});
|
|
1299
2236
|
el.addEventListener("blur", () => this.commitBlock(el));
|
|
1300
2237
|
el.addEventListener("keydown", (ev) => this.onKeydown(el, ev));
|
|
2238
|
+
const stamped = el.getAttribute("data-hf-anchor");
|
|
2239
|
+
if (stamped && this.region?.contains(el)) this.region.adoptBlock(el, stamped);
|
|
1301
2240
|
}
|
|
1302
2241
|
/**
|
|
1303
2242
|
* Replace `oldEl` with `newNodes`, suppressing the re-entrant blur→commit that removing a focused
|
|
@@ -1327,7 +2266,7 @@ var DocxodusEditor = (() => {
|
|
|
1327
2266
|
if (this.closed || this.replacing) return;
|
|
1328
2267
|
const unid = el.getAttribute("data-anchor");
|
|
1329
2268
|
if (!unid) return;
|
|
1330
|
-
const fullId = this.
|
|
2269
|
+
const fullId = this.anchorIdOf(el);
|
|
1331
2270
|
if (!fullId) return;
|
|
1332
2271
|
const result = this.commitTextChange(el, fullId);
|
|
1333
2272
|
if (!result) return;
|
|
@@ -1354,9 +2293,14 @@ var DocxodusEditor = (() => {
|
|
|
1354
2293
|
);
|
|
1355
2294
|
if (html.charCodeAt(0) !== 123) {
|
|
1356
2295
|
const fresh = new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
2296
|
+
const inBand = this.isBandBlock(el);
|
|
1357
2297
|
if (fresh && this.replaceNode(el, fresh)) {
|
|
1358
|
-
|
|
1359
|
-
|
|
2298
|
+
if (inBand) {
|
|
2299
|
+
this.region.adoptBlock(fresh, newAnchor);
|
|
2300
|
+
} else {
|
|
2301
|
+
this.unidToFullId.delete(unid);
|
|
2302
|
+
this.unidToFullId.set(newUnid, newAnchor);
|
|
2303
|
+
}
|
|
1360
2304
|
this.wireBlock(fresh);
|
|
1361
2305
|
if (this.activeBlock === el) this.activeBlock = fresh;
|
|
1362
2306
|
}
|
|
@@ -1432,7 +2376,7 @@ var DocxodusEditor = (() => {
|
|
|
1432
2376
|
const rawOffset = caretOffsetIn(el);
|
|
1433
2377
|
const unid = el.getAttribute("data-anchor");
|
|
1434
2378
|
if (rawOffset == null || !unid) return;
|
|
1435
|
-
let fullId = this.
|
|
2379
|
+
let fullId = this.anchorIdOf(el);
|
|
1436
2380
|
if (!fullId) return;
|
|
1437
2381
|
const offset = trimmedSplitOffset(el, rawOffset);
|
|
1438
2382
|
const idx = this.blockIndex(el);
|
|
@@ -1444,17 +2388,23 @@ var DocxodusEditor = (() => {
|
|
|
1444
2388
|
const second = res.created?.[0];
|
|
1445
2389
|
if (!first || !second) return;
|
|
1446
2390
|
if (this.affectsList(res) || wrappedInBorder) {
|
|
1447
|
-
this.
|
|
2391
|
+
this.refreshAfter(el, idx + 1, false);
|
|
1448
2392
|
this.options.onEdit?.({ anchorId: second.id, unid: second.unid });
|
|
1449
2393
|
return;
|
|
1450
2394
|
}
|
|
1451
2395
|
const firstEl = this.renderInto(first.id);
|
|
1452
2396
|
const secondEl = this.renderInto(second.id);
|
|
1453
2397
|
if (!firstEl || !secondEl) return;
|
|
2398
|
+
const inBand = this.isBandBlock(el);
|
|
1454
2399
|
if (!this.replaceNode(el, firstEl, secondEl)) return;
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2400
|
+
if (inBand) {
|
|
2401
|
+
this.region.adoptBlock(firstEl, first.id);
|
|
2402
|
+
this.region.adoptBlock(secondEl, second.id);
|
|
2403
|
+
} else {
|
|
2404
|
+
this.unidToFullId.delete(unid);
|
|
2405
|
+
this.unidToFullId.set(first.unid, first.id);
|
|
2406
|
+
this.unidToFullId.set(second.unid, second.id);
|
|
2407
|
+
}
|
|
1458
2408
|
this.wireBlock(firstEl);
|
|
1459
2409
|
this.wireBlock(secondEl);
|
|
1460
2410
|
placeCaretAtOffset(secondEl, 0);
|
|
@@ -1465,8 +2415,8 @@ var DocxodusEditor = (() => {
|
|
|
1465
2415
|
const prevUnid = prev.getAttribute("data-anchor");
|
|
1466
2416
|
const thisUnid = el.getAttribute("data-anchor");
|
|
1467
2417
|
if (!prevUnid || !thisUnid) return;
|
|
1468
|
-
let prevId = this.
|
|
1469
|
-
let thisId = this.
|
|
2418
|
+
let prevId = this.anchorIdOf(prev);
|
|
2419
|
+
let thisId = this.anchorIdOf(el);
|
|
1470
2420
|
if (!prevId || !thisId) return;
|
|
1471
2421
|
const prevIdx = this.blockIndex(prev);
|
|
1472
2422
|
const wrappedInBorder = inBorderWrapper(prev) || inBorderWrapper(el);
|
|
@@ -1478,17 +2428,22 @@ var DocxodusEditor = (() => {
|
|
|
1478
2428
|
const merged = res.modified?.[0];
|
|
1479
2429
|
if (!merged) return;
|
|
1480
2430
|
if (this.affectsList(res) || wrappedInBorder) {
|
|
1481
|
-
this.
|
|
2431
|
+
this.refreshAfter(el, prevIdx, true);
|
|
1482
2432
|
this.options.onEdit?.({ anchorId: merged.id, unid: merged.unid });
|
|
1483
2433
|
return;
|
|
1484
2434
|
}
|
|
1485
2435
|
const mergedEl = this.renderInto(merged.id);
|
|
1486
2436
|
if (!mergedEl) return;
|
|
2437
|
+
const inBand = this.isBandBlock(prev);
|
|
1487
2438
|
if (!this.replaceNode(prev, mergedEl)) return;
|
|
1488
2439
|
el.remove();
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2440
|
+
if (inBand) {
|
|
2441
|
+
this.region.adoptBlock(mergedEl, merged.id);
|
|
2442
|
+
} else {
|
|
2443
|
+
this.unidToFullId.delete(prevUnid);
|
|
2444
|
+
this.unidToFullId.delete(thisUnid);
|
|
2445
|
+
this.unidToFullId.set(merged.unid, merged.id);
|
|
2446
|
+
}
|
|
1492
2447
|
this.wireBlock(mergedEl);
|
|
1493
2448
|
placeCaretAtOffset(mergedEl, caret);
|
|
1494
2449
|
this.options.onEdit?.({ anchorId: merged.id, unid: merged.unid });
|
|
@@ -1551,10 +2506,10 @@ var DocxodusEditor = (() => {
|
|
|
1551
2506
|
if (html.charCodeAt(0) === 123) return null;
|
|
1552
2507
|
return new DOMParser().parseFromString(html, "text/html").body.firstElementChild;
|
|
1553
2508
|
}
|
|
1554
|
-
/** The editable block immediately before `el`
|
|
2509
|
+
/** The editable block immediately before `el` within its own root, or null. */
|
|
1555
2510
|
previousEditable(el) {
|
|
1556
2511
|
const all = Array.from(
|
|
1557
|
-
this.
|
|
2512
|
+
this.ownerRoot(el).querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
1558
2513
|
);
|
|
1559
2514
|
const i = all.indexOf(el);
|
|
1560
2515
|
return i > 0 ? all[i - 1] : null;
|
|
@@ -1574,8 +2529,9 @@ var DocxodusEditor = (() => {
|
|
|
1574
2529
|
* A collapsed or single-block selection yields just the active block. */
|
|
1575
2530
|
selectedBlocks() {
|
|
1576
2531
|
const sel = typeof window !== "undefined" ? window.getSelection() : null;
|
|
2532
|
+
const root = this.activeBlock ? this.ownerRoot(this.activeBlock) : this.editRoot;
|
|
1577
2533
|
const all = Array.from(
|
|
1578
|
-
|
|
2534
|
+
root.querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
1579
2535
|
);
|
|
1580
2536
|
if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
|
|
1581
2537
|
const range = sel.getRangeAt(0);
|
|
@@ -1605,13 +2561,13 @@ var DocxodusEditor = (() => {
|
|
|
1605
2561
|
const contentLen = blockContentText(block).length;
|
|
1606
2562
|
if (hasStart) {
|
|
1607
2563
|
const start = contentOffsetOf(block, range.startContainer, range.startOffset);
|
|
1608
|
-
return { start, length: Math.max(0, contentLen - start) };
|
|
2564
|
+
return trimmedSpan(block, { start, length: Math.max(0, contentLen - start) });
|
|
1609
2565
|
}
|
|
1610
2566
|
if (hasEnd) {
|
|
1611
2567
|
const end = contentOffsetOf(block, range.endContainer, range.endOffset);
|
|
1612
|
-
return { start: 0, length: end };
|
|
2568
|
+
return trimmedSpan(block, { start: 0, length: end });
|
|
1613
2569
|
}
|
|
1614
|
-
return { start: 0, length: contentLen };
|
|
2570
|
+
return trimmedSpan(block, { start: 0, length: contentLen });
|
|
1615
2571
|
}
|
|
1616
2572
|
/** Apply an inline ApplyFormat op to each block's slice of the selection, then reconcile
|
|
1617
2573
|
* the DOM incrementally (see {@link finishMultiBlockOp} — a full remount costs a whole-
|
|
@@ -1658,7 +2614,7 @@ var DocxodusEditor = (() => {
|
|
|
1658
2614
|
return {
|
|
1659
2615
|
block: b,
|
|
1660
2616
|
unid,
|
|
1661
|
-
fullId:
|
|
2617
|
+
fullId: this.anchorIdOf(b),
|
|
1662
2618
|
span: this.blockSpanForSelection(b),
|
|
1663
2619
|
res: null
|
|
1664
2620
|
};
|
|
@@ -1678,7 +2634,7 @@ var DocxodusEditor = (() => {
|
|
|
1678
2634
|
const edited = targets.filter((t) => t.res && t.unid);
|
|
1679
2635
|
if (edited.length === 0) return;
|
|
1680
2636
|
if (forceRemount || this.options.paginated || edited.some((t) => this.affectsList(t.res))) {
|
|
1681
|
-
this.
|
|
2637
|
+
this.refreshAfter(edited[0].block, -1, false);
|
|
1682
2638
|
return;
|
|
1683
2639
|
}
|
|
1684
2640
|
const swapped = [];
|
|
@@ -1721,7 +2677,7 @@ var DocxodusEditor = (() => {
|
|
|
1721
2677
|
}
|
|
1722
2678
|
const unid = block.getAttribute("data-anchor");
|
|
1723
2679
|
if (!unid) return;
|
|
1724
|
-
let fullId = this.
|
|
2680
|
+
let fullId = this.anchorIdOf(block);
|
|
1725
2681
|
if (!fullId) return;
|
|
1726
2682
|
const span = selectionSpanIn(block);
|
|
1727
2683
|
const on = value ?? !selectionHasFormat(key, block);
|
|
@@ -1737,7 +2693,7 @@ var DocxodusEditor = (() => {
|
|
|
1737
2693
|
);
|
|
1738
2694
|
if (!res.success) return;
|
|
1739
2695
|
if (this.affectsList(res)) {
|
|
1740
|
-
this.
|
|
2696
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1741
2697
|
return;
|
|
1742
2698
|
}
|
|
1743
2699
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1756,7 +2712,7 @@ var DocxodusEditor = (() => {
|
|
|
1756
2712
|
if (blocks.length > 1 && this.applyInlineOpAcrossBlocks(blocks, { fontSizePts: pts })) return;
|
|
1757
2713
|
const unid = block.getAttribute("data-anchor");
|
|
1758
2714
|
if (!unid) return;
|
|
1759
|
-
let fullId = this.
|
|
2715
|
+
let fullId = this.anchorIdOf(block);
|
|
1760
2716
|
if (!fullId) return;
|
|
1761
2717
|
let span = selectionSpanIn(block);
|
|
1762
2718
|
if (!span && this.lastSelection && this.lastSelection.unid === unid) span = this.lastSelection.span;
|
|
@@ -1771,7 +2727,7 @@ var DocxodusEditor = (() => {
|
|
|
1771
2727
|
);
|
|
1772
2728
|
if (!res.success) return;
|
|
1773
2729
|
if (this.affectsList(res)) {
|
|
1774
|
-
this.
|
|
2730
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1775
2731
|
return;
|
|
1776
2732
|
}
|
|
1777
2733
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1792,7 +2748,7 @@ var DocxodusEditor = (() => {
|
|
|
1792
2748
|
if (blocks.length > 1 && this.applyInlineOpAcrossBlocks(blocks, { fontFamily: name })) return;
|
|
1793
2749
|
const unid = block.getAttribute("data-anchor");
|
|
1794
2750
|
if (!unid) return;
|
|
1795
|
-
let fullId = this.
|
|
2751
|
+
let fullId = this.anchorIdOf(block);
|
|
1796
2752
|
if (!fullId) return;
|
|
1797
2753
|
let span = selectionSpanIn(block);
|
|
1798
2754
|
if (!span && this.lastSelection && this.lastSelection.unid === unid) span = this.lastSelection.span;
|
|
@@ -1807,7 +2763,7 @@ var DocxodusEditor = (() => {
|
|
|
1807
2763
|
);
|
|
1808
2764
|
if (!res.success) return;
|
|
1809
2765
|
if (this.affectsList(res)) {
|
|
1810
|
-
this.
|
|
2766
|
+
this.refreshAfter(block, this.blockIndex(block), false);
|
|
1811
2767
|
return;
|
|
1812
2768
|
}
|
|
1813
2769
|
const fresh = this.swapBlock(block, unid, res.modified?.[0]);
|
|
@@ -1828,7 +2784,7 @@ var DocxodusEditor = (() => {
|
|
|
1828
2784
|
if (this.closed || !block) return;
|
|
1829
2785
|
const unid = block.getAttribute("data-anchor");
|
|
1830
2786
|
if (!unid) return;
|
|
1831
|
-
let fullId = this.
|
|
2787
|
+
let fullId = this.anchorIdOf(block);
|
|
1832
2788
|
if (!fullId) return;
|
|
1833
2789
|
const idx = this.blockIndex(block);
|
|
1834
2790
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -1841,7 +2797,7 @@ var DocxodusEditor = (() => {
|
|
|
1841
2797
|
)
|
|
1842
2798
|
);
|
|
1843
2799
|
if (!res.success) return;
|
|
1844
|
-
this.
|
|
2800
|
+
this.refreshAfter(block, idx, false);
|
|
1845
2801
|
}
|
|
1846
2802
|
/**
|
|
1847
2803
|
* Insert a `rows`×`cols` table after the active block. `options.cellContents` (row-major
|
|
@@ -1853,7 +2809,7 @@ var DocxodusEditor = (() => {
|
|
|
1853
2809
|
if (this.closed || !block) return;
|
|
1854
2810
|
const unid = block.getAttribute("data-anchor");
|
|
1855
2811
|
if (!unid) return;
|
|
1856
|
-
let fullId = this.
|
|
2812
|
+
let fullId = this.anchorIdOf(block);
|
|
1857
2813
|
if (!fullId) return;
|
|
1858
2814
|
const idx = this.blockIndex(block);
|
|
1859
2815
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -1869,7 +2825,7 @@ var DocxodusEditor = (() => {
|
|
|
1869
2825
|
)
|
|
1870
2826
|
);
|
|
1871
2827
|
if (!res.success) return;
|
|
1872
|
-
this.
|
|
2828
|
+
this.refreshAfter(block, idx, false);
|
|
1873
2829
|
}
|
|
1874
2830
|
// ─── Table row / column editing (active block must be inside a table cell) ──────────
|
|
1875
2831
|
/** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */
|
|
@@ -1878,13 +2834,13 @@ var DocxodusEditor = (() => {
|
|
|
1878
2834
|
if (this.closed || !block || !block.closest("table")) return;
|
|
1879
2835
|
const unid = block.getAttribute("data-anchor");
|
|
1880
2836
|
if (!unid) return;
|
|
1881
|
-
let fullId = this.
|
|
2837
|
+
let fullId = this.anchorIdOf(block);
|
|
1882
2838
|
if (!fullId) return;
|
|
1883
2839
|
const idx = this.blockIndex(block);
|
|
1884
2840
|
fullId = this.syncBlock(block, fullId);
|
|
1885
2841
|
const res = this.parseEdit(run(fullId));
|
|
1886
2842
|
if (!res.success) return;
|
|
1887
|
-
this.
|
|
2843
|
+
this.refreshAfter(block, idx, false);
|
|
1888
2844
|
}
|
|
1889
2845
|
/** Insert a row above/below the active cell's row. No-op outside a table. */
|
|
1890
2846
|
insertTableRow(where) {
|
|
@@ -1925,13 +2881,13 @@ var DocxodusEditor = (() => {
|
|
|
1925
2881
|
if (this.closed || !block) return;
|
|
1926
2882
|
const unid = block.getAttribute("data-anchor");
|
|
1927
2883
|
if (!unid) return;
|
|
1928
|
-
let fullId = this.
|
|
2884
|
+
let fullId = this.anchorIdOf(block);
|
|
1929
2885
|
if (!fullId) return;
|
|
1930
2886
|
const idx = this.blockIndex(block);
|
|
1931
2887
|
fullId = this.syncBlock(block, fullId);
|
|
1932
2888
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetListLevel(this.handle, fullId, delta));
|
|
1933
2889
|
if (!res.success) return;
|
|
1934
|
-
this.
|
|
2890
|
+
this.refreshAfter(block, idx, false);
|
|
1935
2891
|
}
|
|
1936
2892
|
/** Toggle (or set) page-break-before on the active block. */
|
|
1937
2893
|
pageBreakBefore(value = true) {
|
|
@@ -1946,7 +2902,7 @@ var DocxodusEditor = (() => {
|
|
|
1946
2902
|
if (this.closed || !block) return;
|
|
1947
2903
|
const unid = block.getAttribute("data-anchor");
|
|
1948
2904
|
if (!unid) return;
|
|
1949
|
-
let fullId = this.
|
|
2905
|
+
let fullId = this.anchorIdOf(block);
|
|
1950
2906
|
if (!fullId) return;
|
|
1951
2907
|
let membership = null;
|
|
1952
2908
|
try {
|
|
@@ -1960,7 +2916,7 @@ var DocxodusEditor = (() => {
|
|
|
1960
2916
|
this.exports.DocxSessionBridge.ApplyListFormat(this.handle, fullId, isThisKind ? "none" : kind)
|
|
1961
2917
|
);
|
|
1962
2918
|
if (!res.success) return;
|
|
1963
|
-
this.
|
|
2919
|
+
this.refreshAfter(block, idx, false);
|
|
1964
2920
|
}
|
|
1965
2921
|
/** Clear all paragraph borders (e.g. remove an inserted horizontal rule) on the active block —
|
|
1966
2922
|
* or every block in a multi-block selection. The engine/wire already accept `clearBorders`;
|
|
@@ -1982,12 +2938,12 @@ var DocxodusEditor = (() => {
|
|
|
1982
2938
|
if (this.editableList().length <= 1) return;
|
|
1983
2939
|
const unid = block.getAttribute("data-anchor");
|
|
1984
2940
|
if (!unid) return;
|
|
1985
|
-
const fullId = this.
|
|
2941
|
+
const fullId = this.anchorIdOf(block);
|
|
1986
2942
|
if (!fullId) return;
|
|
1987
2943
|
const idx = this.blockIndex(block);
|
|
1988
2944
|
const res = this.parseEdit(this.exports.DocxSessionBridge.DeleteBlock(this.handle, fullId));
|
|
1989
2945
|
if (!res.success) return;
|
|
1990
|
-
this.
|
|
2946
|
+
this.refreshAfter(block, Math.max(0, idx - 1), true);
|
|
1991
2947
|
}
|
|
1992
2948
|
applyParagraphFormat(op) {
|
|
1993
2949
|
const block = this.activeBlock;
|
|
@@ -2002,7 +2958,7 @@ var DocxodusEditor = (() => {
|
|
|
2002
2958
|
return;
|
|
2003
2959
|
const unid = block.getAttribute("data-anchor");
|
|
2004
2960
|
if (!unid) return;
|
|
2005
|
-
let fullId = this.
|
|
2961
|
+
let fullId = this.anchorIdOf(block);
|
|
2006
2962
|
if (!fullId) return;
|
|
2007
2963
|
const idx = this.blockIndex(block);
|
|
2008
2964
|
fullId = this.syncBlock(block, fullId);
|
|
@@ -2011,7 +2967,7 @@ var DocxodusEditor = (() => {
|
|
|
2011
2967
|
);
|
|
2012
2968
|
if (!res.success) return;
|
|
2013
2969
|
if (this.affectsList(res) || op.clearBorders) {
|
|
2014
|
-
this.
|
|
2970
|
+
this.refreshAfter(block, idx, false);
|
|
2015
2971
|
return;
|
|
2016
2972
|
}
|
|
2017
2973
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
@@ -2029,14 +2985,14 @@ var DocxodusEditor = (() => {
|
|
|
2029
2985
|
return;
|
|
2030
2986
|
const unid = block.getAttribute("data-anchor");
|
|
2031
2987
|
if (!unid) return;
|
|
2032
|
-
let fullId = this.
|
|
2988
|
+
let fullId = this.anchorIdOf(block);
|
|
2033
2989
|
if (!fullId) return;
|
|
2034
2990
|
const idx = this.blockIndex(block);
|
|
2035
2991
|
fullId = this.syncBlock(block, fullId);
|
|
2036
2992
|
const res = this.parseEdit(this.exports.DocxSessionBridge.SetParagraphStyle(this.handle, fullId, styleId));
|
|
2037
2993
|
if (!res.success) return;
|
|
2038
2994
|
if (this.affectsList(res)) {
|
|
2039
|
-
this.
|
|
2995
|
+
this.refreshAfter(block, idx, false);
|
|
2040
2996
|
return;
|
|
2041
2997
|
}
|
|
2042
2998
|
this.swapBlock(block, unid, res.modified?.[0])?.focus();
|
|
@@ -2051,6 +3007,37 @@ var DocxodusEditor = (() => {
|
|
|
2051
3007
|
if (this.closed) return;
|
|
2052
3008
|
if (this.exports.DocxSessionBridge.Redo(this.handle)) this.remount();
|
|
2053
3009
|
}
|
|
3010
|
+
// ─── Header/footer region commands (no-ops unless `headerFooter` is on) ───────────────
|
|
3011
|
+
/**
|
|
3012
|
+
* Select which story kind a band edits (`"default"` / `"first"` / `"even"`). A kind with no
|
|
3013
|
+
* existing part is created empty, so the band always presents something editable. `"first"`
|
|
3014
|
+
* sets the section's `w:titlePg`; `"even"` sets the document-global `w:evenAndOddHeaders`
|
|
3015
|
+
* (which also governs footers — the band surfaces that caveat inline).
|
|
3016
|
+
*/
|
|
3017
|
+
setHeaderFooterKind(which, kind) {
|
|
3018
|
+
this.assertOpen();
|
|
3019
|
+
this.region?.setKind(which, kind);
|
|
3020
|
+
}
|
|
3021
|
+
/** The story kind a band is currently editing, or null when the region is off. */
|
|
3022
|
+
headerFooterKind(which) {
|
|
3023
|
+
return this.region?.kindOf(which) ?? null;
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* Append a page-number field to the focused header/footer story paragraph (falling back to the
|
|
3027
|
+
* band's last paragraph — Word's convention). No-op outside a band.
|
|
3028
|
+
*/
|
|
3029
|
+
insertPageNumber(field = "currentPage") {
|
|
3030
|
+
this.assertOpen();
|
|
3031
|
+
if (!this.region) return;
|
|
3032
|
+
const block = this.activeBlock;
|
|
3033
|
+
const band = block ? this.region.bandOf(block) : null;
|
|
3034
|
+
const anchorId = block?.getAttribute("data-hf-anchor");
|
|
3035
|
+
if (band && anchorId) {
|
|
3036
|
+
this.region.insertPageNumber(this.region.whichOf(band), anchorId, field);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
this.region.insertPageNumberInBand("footer", field);
|
|
3040
|
+
}
|
|
2054
3041
|
/** Which inline formats the current selection carries — for ribbon button highlighting. */
|
|
2055
3042
|
queryFormatState() {
|
|
2056
3043
|
const block = this.activeBlock ?? this.editRoot;
|
|
@@ -2066,13 +3053,17 @@ var DocxodusEditor = (() => {
|
|
|
2066
3053
|
}
|
|
2067
3054
|
/** Re-render one block from the live session by EditResult ref, swapping it in place. */
|
|
2068
3055
|
swapBlock(oldEl, oldUnid, ref) {
|
|
2069
|
-
const
|
|
3056
|
+
const inBand = this.isBandBlock(oldEl);
|
|
3057
|
+
const anchorId = ref?.id ?? this.anchorIdOf(oldEl);
|
|
2070
3058
|
const newUnid = ref?.unid ?? oldUnid;
|
|
2071
3059
|
if (!anchorId) return null;
|
|
2072
3060
|
const fresh = this.renderInto(anchorId);
|
|
2073
3061
|
if (!fresh || !this.replaceNode(oldEl, fresh)) return null;
|
|
2074
|
-
|
|
2075
|
-
|
|
3062
|
+
if (!inBand) {
|
|
3063
|
+
this.unidToFullId.delete(oldUnid);
|
|
3064
|
+
this.unidToFullId.set(newUnid, anchorId);
|
|
3065
|
+
}
|
|
3066
|
+
if (inBand) this.region.adoptBlock(fresh, anchorId);
|
|
2076
3067
|
this.wireBlock(fresh);
|
|
2077
3068
|
this.activeBlock = fresh;
|
|
2078
3069
|
this.options.onEdit?.({ anchorId, unid: newUnid });
|
|
@@ -2102,12 +3093,14 @@ var DocxodusEditor = (() => {
|
|
|
2102
3093
|
...completeArgs(bytes, this.options.cssPrefix, this.options.fabricateClasses, this.options.paginated, this.options.scale)
|
|
2103
3094
|
);
|
|
2104
3095
|
}
|
|
2105
|
-
/** Editable blocks in document order. */
|
|
3096
|
+
/** Editable BODY blocks in document order (band blocks are enumerated by `ownerRoot`). */
|
|
2106
3097
|
editableList() {
|
|
2107
3098
|
return Array.from(this.editRoot.querySelectorAll('[data-anchor][contenteditable="true"]'));
|
|
2108
3099
|
}
|
|
2109
3100
|
blockIndex(el) {
|
|
2110
|
-
return
|
|
3101
|
+
return Array.from(
|
|
3102
|
+
this.ownerRoot(el).querySelectorAll('[data-anchor][contenteditable="true"]')
|
|
3103
|
+
).indexOf(el);
|
|
2111
3104
|
}
|
|
2112
3105
|
/**
|
|
2113
3106
|
* True when an edit produced or touched a list item (kind "li"). List markers and
|
|
@@ -2137,6 +3130,7 @@ var DocxodusEditor = (() => {
|
|
|
2137
3130
|
placeCaretAtOffset(target, caretAtEnd ? (target.textContent ?? "").length : 0);
|
|
2138
3131
|
}
|
|
2139
3132
|
}
|
|
3133
|
+
this.syncRegionToBody(this.activeBlock ?? void 0);
|
|
2140
3134
|
}
|
|
2141
3135
|
};
|
|
2142
3136
|
return __toCommonJS(editor_exports);
|