@kedataindo/docflow-layout-engine 0.0.1 → 0.0.3
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/LICENSE +44 -0
- package/dist/index.cjs +182 -5
- package/dist/index.d.cts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +181 -5
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
DocFlow — Proprietary EULA Reference
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 PT Kedata Indonesia (the "Licensor"). All rights reserved.
|
|
4
|
+
|
|
5
|
+
────────────────────────────────────────────────────────────────────────
|
|
6
|
+
THIS IS A TEMPLATE / SCAFFOLD FILE.
|
|
7
|
+
────────────────────────────────────────────────────────────────────────
|
|
8
|
+
This document is a placeholder summarising the licensing model. The
|
|
9
|
+
binding text is the **DocFlow End-User License Agreement (EULA)** to
|
|
10
|
+
be supplied separately to each customer, paired with a signed Order
|
|
11
|
+
Form (per-deal price + tier + connection cap + server-instance cap).
|
|
12
|
+
Until the EULA is reviewed by counsel and published, you may treat
|
|
13
|
+
this file as the canonical statement that:
|
|
14
|
+
|
|
15
|
+
1. The DocFlow software is proprietary and NOT open-source.
|
|
16
|
+
2. No rights are granted by viewing the source code unless the
|
|
17
|
+
customer has accepted the EULA in writing.
|
|
18
|
+
3. The bundled third-party components listed in `NOTICE` keep
|
|
19
|
+
their original open-source licenses (mostly MIT).
|
|
20
|
+
|
|
21
|
+
The "MIT" placeholder in `README.md` (carried over from before the
|
|
22
|
+
proprietary decision) is **incorrect** and is updated by PR #90
|
|
23
|
+
(replace "MIT" → "Proprietary — see LICENSE + NOTICE").
|
|
24
|
+
────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
Licensing model (per PRD §5 — proprietary EULA + separate MSA):
|
|
27
|
+
|
|
28
|
+
• DocFlow EULA (End-User License Agreement) — perpetual,
|
|
29
|
+
non-exclusive, non-transferable, metered per simultaneous
|
|
30
|
+
connection per server instance. Standard tiers: Community (free,
|
|
31
|
+
no warranty), Professional, Enterprise, Government. Hosting
|
|
32
|
+
DocFlow as a third-party SaaS is **prohibited** without a
|
|
33
|
+
separate OEM/SaaS agreement.
|
|
34
|
+
• Maintenance & Support Agreement (MSA) — annual, separate from
|
|
35
|
+
the EULA. Required for Professional / Enterprise / Government
|
|
36
|
+
for SLA-backed support; optional for Community.
|
|
37
|
+
• Source-code escrow (Government tier) — source code is deposited
|
|
38
|
+
with an escrow agent; released to the licensee if the Licensor
|
|
39
|
+
ceases to operate or fails the maintenance SLA.
|
|
40
|
+
|
|
41
|
+
Governing law: Republic of Indonesia (UU Hak Cipta No. 28/2014).
|
|
42
|
+
Dispute venue: District Court of Central Jakarta.
|
|
43
|
+
|
|
44
|
+
For licensing questions: legal@kedata.co
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
PAGE_SIZES: () => PAGE_SIZES,
|
|
24
24
|
PageBreaker: () => PageBreaker,
|
|
25
25
|
PageLayout: () => PageLayout,
|
|
26
|
+
VirtualPageOverlay: () => VirtualPageOverlay,
|
|
26
27
|
getPageSize: () => getPageSize
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -191,6 +192,7 @@ var PageLayout = class {
|
|
|
191
192
|
resizeObserver = null;
|
|
192
193
|
debounceTimer = null;
|
|
193
194
|
lastDocHash = "";
|
|
195
|
+
lastBlockHashes = /* @__PURE__ */ new Map();
|
|
194
196
|
lastShadowHash = "";
|
|
195
197
|
lastSize = { width: 0, height: 0 };
|
|
196
198
|
lastResult = null;
|
|
@@ -288,6 +290,7 @@ var PageLayout = class {
|
|
|
288
290
|
this.shadowRoot = null;
|
|
289
291
|
this.lastResult = null;
|
|
290
292
|
this.lastDocHash = "";
|
|
293
|
+
this.lastBlockHashes = /* @__PURE__ */ new Map();
|
|
291
294
|
this.lastShadowHash = "";
|
|
292
295
|
}
|
|
293
296
|
scheduleLayout() {
|
|
@@ -301,20 +304,56 @@ var PageLayout = class {
|
|
|
301
304
|
clearTimeout(this.debounceTimer);
|
|
302
305
|
this.debounceTimer = null;
|
|
303
306
|
}
|
|
304
|
-
const docHash = this.computeDocHash();
|
|
305
307
|
const size = this.getContainerSize();
|
|
306
|
-
|
|
308
|
+
const containerChanged = this.lastSize.width !== size.width || this.lastSize.height !== size.height;
|
|
309
|
+
const docHash = this.computeDocHash();
|
|
310
|
+
if (this.lastResult && !containerChanged && this.lastDocHash === docHash) {
|
|
307
311
|
this.resolveAll(this.lastResult);
|
|
308
312
|
return this.lastResult;
|
|
309
313
|
}
|
|
310
314
|
this.prepareShadowLayout();
|
|
311
|
-
const
|
|
312
|
-
const blocks = this.measureBlocks(root);
|
|
315
|
+
const blocks = this.measureBlocks(this.element);
|
|
313
316
|
const availableHeight = this.availableHeight();
|
|
314
|
-
const
|
|
317
|
+
const newBlockHashes = this.computeBlockHashes();
|
|
318
|
+
const allPages = this.breaker.computePages(blocks, availableHeight, this.options.maxCharsPerPage);
|
|
319
|
+
let pages;
|
|
320
|
+
if (this.lastResult && !containerChanged && this.lastBlockHashes.size > 0) {
|
|
321
|
+
let firstChangedBlockIdx = -1;
|
|
322
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
323
|
+
if (newBlockHashes.get(i) !== this.lastBlockHashes.get(i)) {
|
|
324
|
+
firstChangedBlockIdx = i;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (firstChangedBlockIdx === -1 && newBlockHashes.size !== this.lastBlockHashes.size) {
|
|
329
|
+
firstChangedBlockIdx = Math.min(newBlockHashes.size, this.lastBlockHashes.size);
|
|
330
|
+
}
|
|
331
|
+
if (firstChangedBlockIdx >= 0) {
|
|
332
|
+
const firstChangedFrom = blocks[firstChangedBlockIdx]?.from ?? 0;
|
|
333
|
+
const oldPages = this.lastResult.pages;
|
|
334
|
+
let affectedPageIdx = oldPages.length;
|
|
335
|
+
for (let p = 0; p < oldPages.length; p++) {
|
|
336
|
+
const lastBlock = oldPages[p].blocks[oldPages[p].blocks.length - 1];
|
|
337
|
+
if (lastBlock && lastBlock.to >= firstChangedFrom) {
|
|
338
|
+
affectedPageIdx = p;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (affectedPageIdx > 0 && affectedPageIdx < allPages.length && oldPages[affectedPageIdx - 1]?.to === allPages[affectedPageIdx - 1]?.to) {
|
|
343
|
+
pages = [...oldPages.slice(0, affectedPageIdx), ...allPages.slice(affectedPageIdx)];
|
|
344
|
+
} else {
|
|
345
|
+
pages = allPages;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
pages = allPages;
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
pages = allPages;
|
|
352
|
+
}
|
|
315
353
|
const result = { pages, invalidatedAt: Date.now() };
|
|
316
354
|
this.lastResult = result;
|
|
317
355
|
this.lastDocHash = docHash;
|
|
356
|
+
this.lastBlockHashes = newBlockHashes;
|
|
318
357
|
this.lastSize = size;
|
|
319
358
|
this.resolveAll(result);
|
|
320
359
|
return result;
|
|
@@ -334,6 +373,18 @@ var PageLayout = class {
|
|
|
334
373
|
}
|
|
335
374
|
return hash;
|
|
336
375
|
}
|
|
376
|
+
/** Compute per-block hashes for incremental recalculation. */
|
|
377
|
+
computeBlockHashes() {
|
|
378
|
+
const map = /* @__PURE__ */ new Map();
|
|
379
|
+
let idx = 0;
|
|
380
|
+
for (const child of Array.from(this.element.children)) {
|
|
381
|
+
if (child instanceof HTMLElement) {
|
|
382
|
+
map.set(idx, child.tagName + (child.getAttribute("data-from") ?? "") + (child.getAttribute("data-to") ?? "") + (child.getAttribute("data-node-type") ?? "") + (child.textContent ?? ""));
|
|
383
|
+
}
|
|
384
|
+
idx++;
|
|
385
|
+
}
|
|
386
|
+
return map;
|
|
387
|
+
}
|
|
337
388
|
getContainerSize() {
|
|
338
389
|
const rect = this.element.getBoundingClientRect();
|
|
339
390
|
return { width: rect.width, height: rect.height };
|
|
@@ -407,10 +458,136 @@ var PageLayout = class {
|
|
|
407
458
|
return null;
|
|
408
459
|
}
|
|
409
460
|
};
|
|
461
|
+
|
|
462
|
+
// src/VirtualPageOverlay.ts
|
|
463
|
+
var VirtualPageOverlay = class {
|
|
464
|
+
pageLayout;
|
|
465
|
+
config;
|
|
466
|
+
scrollEl;
|
|
467
|
+
editorEl;
|
|
468
|
+
bufferPages;
|
|
469
|
+
lastResult = null;
|
|
470
|
+
lastVisible = [];
|
|
471
|
+
lastScrollTop = -1;
|
|
472
|
+
scrollHandler = null;
|
|
473
|
+
rafId = 0;
|
|
474
|
+
constructor(editor, config, scrollEl, bufferPages = 2) {
|
|
475
|
+
const ps = typeof config.pageSize === "string" ? getPageSize(config.pageSize) ?? { id: "a4", name: "A4", pageWidth: 794, pageHeight: 1123 } : config.pageSize;
|
|
476
|
+
this.config = {
|
|
477
|
+
pageSize: ps,
|
|
478
|
+
margins: config.margins ?? { top: 20, bottom: 20, left: 50, right: 50 },
|
|
479
|
+
pageGap: config.pageGap ?? 50,
|
|
480
|
+
headerLeft: config.headerLeft ?? "",
|
|
481
|
+
headerRight: config.headerRight ?? "",
|
|
482
|
+
footerLeft: config.footerLeft ?? "",
|
|
483
|
+
footerRight: config.footerRight ?? ""
|
|
484
|
+
};
|
|
485
|
+
this.scrollEl = scrollEl;
|
|
486
|
+
this.bufferPages = bufferPages;
|
|
487
|
+
this.editorEl = editor instanceof HTMLElement ? editor : editor.dom;
|
|
488
|
+
this.pageLayout = new PageLayout(editor, {
|
|
489
|
+
pageHeight: ps.pageHeight,
|
|
490
|
+
pageWidth: ps.pageWidth,
|
|
491
|
+
margins: this.config.margins
|
|
492
|
+
}, 150);
|
|
493
|
+
}
|
|
494
|
+
/** Run or re-run layout measurement. Resolves with current page data. */
|
|
495
|
+
async layout() {
|
|
496
|
+
const result = await this.pageLayout.layout();
|
|
497
|
+
this.lastResult = result;
|
|
498
|
+
return this.computeData();
|
|
499
|
+
}
|
|
500
|
+
/** Start observing scroll events to track visible pages. */
|
|
501
|
+
observe() {
|
|
502
|
+
if (this.scrollHandler) return;
|
|
503
|
+
this.scrollHandler = () => this.onScroll();
|
|
504
|
+
this.scrollEl.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
505
|
+
this.updateVisible();
|
|
506
|
+
}
|
|
507
|
+
/** Stop observing scroll events. */
|
|
508
|
+
disconnect() {
|
|
509
|
+
if (this.scrollHandler) {
|
|
510
|
+
this.scrollEl.removeEventListener("scroll", this.scrollHandler);
|
|
511
|
+
this.scrollHandler = null;
|
|
512
|
+
}
|
|
513
|
+
if (this.rafId) {
|
|
514
|
+
cancelAnimationFrame(this.rafId);
|
|
515
|
+
this.rafId = 0;
|
|
516
|
+
}
|
|
517
|
+
this.pageLayout.destroy();
|
|
518
|
+
}
|
|
519
|
+
/** Get current page overlay data synchronously (cached). */
|
|
520
|
+
getData() {
|
|
521
|
+
return {
|
|
522
|
+
totalPages: this.lastResult?.pages.length ?? 1,
|
|
523
|
+
visiblePages: this.lastVisible,
|
|
524
|
+
config: this.config
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** Update configuration (page size, margins, etc.) — triggers re-layout. */
|
|
528
|
+
async updateConfig(config) {
|
|
529
|
+
if (config.pageSize) {
|
|
530
|
+
const ps = typeof config.pageSize === "string" ? getPageSize(config.pageSize) ?? this.config.pageSize : config.pageSize;
|
|
531
|
+
this.config.pageSize = ps;
|
|
532
|
+
this.pageLayout.destroy();
|
|
533
|
+
this.pageLayout = new PageLayout(this.editorEl, {
|
|
534
|
+
pageHeight: ps.pageHeight,
|
|
535
|
+
pageWidth: ps.pageWidth,
|
|
536
|
+
margins: this.config.margins
|
|
537
|
+
}, 150);
|
|
538
|
+
}
|
|
539
|
+
if (config.margins) {
|
|
540
|
+
this.config.margins = { ...this.config.margins, ...config.margins };
|
|
541
|
+
}
|
|
542
|
+
if (config.pageGap !== void 0) this.config.pageGap = config.pageGap;
|
|
543
|
+
if (config.headerLeft !== void 0) this.config.headerLeft = config.headerLeft;
|
|
544
|
+
if (config.headerRight !== void 0) this.config.headerRight = config.headerRight;
|
|
545
|
+
if (config.footerLeft !== void 0) this.config.footerLeft = config.footerLeft;
|
|
546
|
+
if (config.footerRight !== void 0) this.config.footerRight = config.footerRight;
|
|
547
|
+
return this.layout();
|
|
548
|
+
}
|
|
549
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
550
|
+
onScroll() {
|
|
551
|
+
if (this.rafId) return;
|
|
552
|
+
this.rafId = requestAnimationFrame(() => {
|
|
553
|
+
this.rafId = 0;
|
|
554
|
+
this.updateVisible();
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
async updateVisible() {
|
|
558
|
+
const st = this.scrollEl.scrollTop;
|
|
559
|
+
if (st === this.lastScrollTop) return;
|
|
560
|
+
this.lastScrollTop = st;
|
|
561
|
+
if (!this.lastResult) {
|
|
562
|
+
await this.layout();
|
|
563
|
+
}
|
|
564
|
+
this.computeData();
|
|
565
|
+
}
|
|
566
|
+
computeData() {
|
|
567
|
+
const pages = this.lastResult?.pages ?? [];
|
|
568
|
+
const totalPages = pages.length || 1;
|
|
569
|
+
const ph = this.config.pageSize.pageHeight + this.config.pageGap;
|
|
570
|
+
const viewportTop = this.scrollEl.scrollTop;
|
|
571
|
+
const viewportBottom = viewportTop + this.scrollEl.clientHeight;
|
|
572
|
+
const visible = [];
|
|
573
|
+
const startPage = Math.max(0, Math.floor(viewportTop / ph) - this.bufferPages);
|
|
574
|
+
const endPage = Math.min(totalPages, Math.ceil(viewportBottom / ph) + this.bufferPages);
|
|
575
|
+
for (let i = startPage; i < endPage; i++) {
|
|
576
|
+
visible.push({
|
|
577
|
+
index: i,
|
|
578
|
+
top: i * ph,
|
|
579
|
+
height: ph
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
this.lastVisible = visible;
|
|
583
|
+
return { totalPages, visiblePages: visible, config: this.config };
|
|
584
|
+
}
|
|
585
|
+
};
|
|
410
586
|
// Annotate the CommonJS export names for ESM import in node:
|
|
411
587
|
0 && (module.exports = {
|
|
412
588
|
PAGE_SIZES,
|
|
413
589
|
PageBreaker,
|
|
414
590
|
PageLayout,
|
|
591
|
+
VirtualPageOverlay,
|
|
415
592
|
getPageSize
|
|
416
593
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -78,6 +78,7 @@ declare class PageLayout {
|
|
|
78
78
|
private resizeObserver;
|
|
79
79
|
private debounceTimer;
|
|
80
80
|
private lastDocHash;
|
|
81
|
+
private lastBlockHashes;
|
|
81
82
|
private lastShadowHash;
|
|
82
83
|
private lastSize;
|
|
83
84
|
private lastResult;
|
|
@@ -91,6 +92,8 @@ declare class PageLayout {
|
|
|
91
92
|
private runLayout;
|
|
92
93
|
private resolveAll;
|
|
93
94
|
private computeDocHash;
|
|
95
|
+
/** Compute per-block hashes for incremental recalculation. */
|
|
96
|
+
private computeBlockHashes;
|
|
94
97
|
private getContainerSize;
|
|
95
98
|
private availableHeight;
|
|
96
99
|
private prepareShadowLayout;
|
|
@@ -99,4 +102,69 @@ declare class PageLayout {
|
|
|
99
102
|
private findTextNodeAtOffset;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
interface PageOverlayConfig {
|
|
106
|
+
pageSize: PageSize | string;
|
|
107
|
+
margins?: {
|
|
108
|
+
top: number;
|
|
109
|
+
bottom: number;
|
|
110
|
+
left: number;
|
|
111
|
+
right: number;
|
|
112
|
+
};
|
|
113
|
+
pageGap?: number;
|
|
114
|
+
headerLeft?: string;
|
|
115
|
+
headerRight?: string;
|
|
116
|
+
footerLeft?: string;
|
|
117
|
+
footerRight?: string;
|
|
118
|
+
}
|
|
119
|
+
interface VisiblePage {
|
|
120
|
+
index: number;
|
|
121
|
+
top: number;
|
|
122
|
+
height: number;
|
|
123
|
+
}
|
|
124
|
+
interface PageOverlayData {
|
|
125
|
+
totalPages: number;
|
|
126
|
+
visiblePages: VisiblePage[];
|
|
127
|
+
config: Required<PageOverlayConfig> & {
|
|
128
|
+
pageSize: PageSize;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Manages virtualized page overlays for a ProseMirror editor.
|
|
133
|
+
*
|
|
134
|
+
* Instead of creating DOM nodes for ALL pages (like tiptap-pagination-plus),
|
|
135
|
+
* this only tracks which pages are currently visible in the viewport and
|
|
136
|
+
* provides the data needed to render a lightweight overlay layer.
|
|
137
|
+
*
|
|
138
|
+
* Usage:
|
|
139
|
+
* const overlay = new VirtualPageOverlay(editor, config, scrollEl, bufferPages?)
|
|
140
|
+
* overlay.layout().then(data => render(data))
|
|
141
|
+
* // On scroll: overlay.updateVisible().then(data => render(data))
|
|
142
|
+
*/
|
|
143
|
+
declare class VirtualPageOverlay {
|
|
144
|
+
pageLayout: PageLayout;
|
|
145
|
+
private config;
|
|
146
|
+
private scrollEl;
|
|
147
|
+
private editorEl;
|
|
148
|
+
private bufferPages;
|
|
149
|
+
private lastResult;
|
|
150
|
+
private lastVisible;
|
|
151
|
+
private lastScrollTop;
|
|
152
|
+
private scrollHandler;
|
|
153
|
+
private rafId;
|
|
154
|
+
constructor(editor: EditorLike | HTMLElement, config: PageOverlayConfig, scrollEl: HTMLElement, bufferPages?: number);
|
|
155
|
+
/** Run or re-run layout measurement. Resolves with current page data. */
|
|
156
|
+
layout(): Promise<PageOverlayData>;
|
|
157
|
+
/** Start observing scroll events to track visible pages. */
|
|
158
|
+
observe(): void;
|
|
159
|
+
/** Stop observing scroll events. */
|
|
160
|
+
disconnect(): void;
|
|
161
|
+
/** Get current page overlay data synchronously (cached). */
|
|
162
|
+
getData(): PageOverlayData;
|
|
163
|
+
/** Update configuration (page size, margins, etc.) — triggers re-layout. */
|
|
164
|
+
updateConfig(config: Partial<PageOverlayConfig>): Promise<PageOverlayData>;
|
|
165
|
+
private onScroll;
|
|
166
|
+
private updateVisible;
|
|
167
|
+
private computeData;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { type BlockInfo, type EditorLike, type LayoutOptions, type LayoutResult, type MeasureTextBlock, PAGE_SIZES, type Page, PageBreaker, PageLayout, type PageOverlayConfig, type PageOverlayData, type PageSize, type SplitResult, VirtualPageOverlay, type VisiblePage, getPageSize };
|
package/dist/index.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ declare class PageLayout {
|
|
|
78
78
|
private resizeObserver;
|
|
79
79
|
private debounceTimer;
|
|
80
80
|
private lastDocHash;
|
|
81
|
+
private lastBlockHashes;
|
|
81
82
|
private lastShadowHash;
|
|
82
83
|
private lastSize;
|
|
83
84
|
private lastResult;
|
|
@@ -91,6 +92,8 @@ declare class PageLayout {
|
|
|
91
92
|
private runLayout;
|
|
92
93
|
private resolveAll;
|
|
93
94
|
private computeDocHash;
|
|
95
|
+
/** Compute per-block hashes for incremental recalculation. */
|
|
96
|
+
private computeBlockHashes;
|
|
94
97
|
private getContainerSize;
|
|
95
98
|
private availableHeight;
|
|
96
99
|
private prepareShadowLayout;
|
|
@@ -99,4 +102,69 @@ declare class PageLayout {
|
|
|
99
102
|
private findTextNodeAtOffset;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
interface PageOverlayConfig {
|
|
106
|
+
pageSize: PageSize | string;
|
|
107
|
+
margins?: {
|
|
108
|
+
top: number;
|
|
109
|
+
bottom: number;
|
|
110
|
+
left: number;
|
|
111
|
+
right: number;
|
|
112
|
+
};
|
|
113
|
+
pageGap?: number;
|
|
114
|
+
headerLeft?: string;
|
|
115
|
+
headerRight?: string;
|
|
116
|
+
footerLeft?: string;
|
|
117
|
+
footerRight?: string;
|
|
118
|
+
}
|
|
119
|
+
interface VisiblePage {
|
|
120
|
+
index: number;
|
|
121
|
+
top: number;
|
|
122
|
+
height: number;
|
|
123
|
+
}
|
|
124
|
+
interface PageOverlayData {
|
|
125
|
+
totalPages: number;
|
|
126
|
+
visiblePages: VisiblePage[];
|
|
127
|
+
config: Required<PageOverlayConfig> & {
|
|
128
|
+
pageSize: PageSize;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Manages virtualized page overlays for a ProseMirror editor.
|
|
133
|
+
*
|
|
134
|
+
* Instead of creating DOM nodes for ALL pages (like tiptap-pagination-plus),
|
|
135
|
+
* this only tracks which pages are currently visible in the viewport and
|
|
136
|
+
* provides the data needed to render a lightweight overlay layer.
|
|
137
|
+
*
|
|
138
|
+
* Usage:
|
|
139
|
+
* const overlay = new VirtualPageOverlay(editor, config, scrollEl, bufferPages?)
|
|
140
|
+
* overlay.layout().then(data => render(data))
|
|
141
|
+
* // On scroll: overlay.updateVisible().then(data => render(data))
|
|
142
|
+
*/
|
|
143
|
+
declare class VirtualPageOverlay {
|
|
144
|
+
pageLayout: PageLayout;
|
|
145
|
+
private config;
|
|
146
|
+
private scrollEl;
|
|
147
|
+
private editorEl;
|
|
148
|
+
private bufferPages;
|
|
149
|
+
private lastResult;
|
|
150
|
+
private lastVisible;
|
|
151
|
+
private lastScrollTop;
|
|
152
|
+
private scrollHandler;
|
|
153
|
+
private rafId;
|
|
154
|
+
constructor(editor: EditorLike | HTMLElement, config: PageOverlayConfig, scrollEl: HTMLElement, bufferPages?: number);
|
|
155
|
+
/** Run or re-run layout measurement. Resolves with current page data. */
|
|
156
|
+
layout(): Promise<PageOverlayData>;
|
|
157
|
+
/** Start observing scroll events to track visible pages. */
|
|
158
|
+
observe(): void;
|
|
159
|
+
/** Stop observing scroll events. */
|
|
160
|
+
disconnect(): void;
|
|
161
|
+
/** Get current page overlay data synchronously (cached). */
|
|
162
|
+
getData(): PageOverlayData;
|
|
163
|
+
/** Update configuration (page size, margins, etc.) — triggers re-layout. */
|
|
164
|
+
updateConfig(config: Partial<PageOverlayConfig>): Promise<PageOverlayData>;
|
|
165
|
+
private onScroll;
|
|
166
|
+
private updateVisible;
|
|
167
|
+
private computeData;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { type BlockInfo, type EditorLike, type LayoutOptions, type LayoutResult, type MeasureTextBlock, PAGE_SIZES, type Page, PageBreaker, PageLayout, type PageOverlayConfig, type PageOverlayData, type PageSize, type SplitResult, VirtualPageOverlay, type VisiblePage, getPageSize };
|
package/dist/index.js
CHANGED
|
@@ -162,6 +162,7 @@ var PageLayout = class {
|
|
|
162
162
|
resizeObserver = null;
|
|
163
163
|
debounceTimer = null;
|
|
164
164
|
lastDocHash = "";
|
|
165
|
+
lastBlockHashes = /* @__PURE__ */ new Map();
|
|
165
166
|
lastShadowHash = "";
|
|
166
167
|
lastSize = { width: 0, height: 0 };
|
|
167
168
|
lastResult = null;
|
|
@@ -259,6 +260,7 @@ var PageLayout = class {
|
|
|
259
260
|
this.shadowRoot = null;
|
|
260
261
|
this.lastResult = null;
|
|
261
262
|
this.lastDocHash = "";
|
|
263
|
+
this.lastBlockHashes = /* @__PURE__ */ new Map();
|
|
262
264
|
this.lastShadowHash = "";
|
|
263
265
|
}
|
|
264
266
|
scheduleLayout() {
|
|
@@ -272,20 +274,56 @@ var PageLayout = class {
|
|
|
272
274
|
clearTimeout(this.debounceTimer);
|
|
273
275
|
this.debounceTimer = null;
|
|
274
276
|
}
|
|
275
|
-
const docHash = this.computeDocHash();
|
|
276
277
|
const size = this.getContainerSize();
|
|
277
|
-
|
|
278
|
+
const containerChanged = this.lastSize.width !== size.width || this.lastSize.height !== size.height;
|
|
279
|
+
const docHash = this.computeDocHash();
|
|
280
|
+
if (this.lastResult && !containerChanged && this.lastDocHash === docHash) {
|
|
278
281
|
this.resolveAll(this.lastResult);
|
|
279
282
|
return this.lastResult;
|
|
280
283
|
}
|
|
281
284
|
this.prepareShadowLayout();
|
|
282
|
-
const
|
|
283
|
-
const blocks = this.measureBlocks(root);
|
|
285
|
+
const blocks = this.measureBlocks(this.element);
|
|
284
286
|
const availableHeight = this.availableHeight();
|
|
285
|
-
const
|
|
287
|
+
const newBlockHashes = this.computeBlockHashes();
|
|
288
|
+
const allPages = this.breaker.computePages(blocks, availableHeight, this.options.maxCharsPerPage);
|
|
289
|
+
let pages;
|
|
290
|
+
if (this.lastResult && !containerChanged && this.lastBlockHashes.size > 0) {
|
|
291
|
+
let firstChangedBlockIdx = -1;
|
|
292
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
293
|
+
if (newBlockHashes.get(i) !== this.lastBlockHashes.get(i)) {
|
|
294
|
+
firstChangedBlockIdx = i;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (firstChangedBlockIdx === -1 && newBlockHashes.size !== this.lastBlockHashes.size) {
|
|
299
|
+
firstChangedBlockIdx = Math.min(newBlockHashes.size, this.lastBlockHashes.size);
|
|
300
|
+
}
|
|
301
|
+
if (firstChangedBlockIdx >= 0) {
|
|
302
|
+
const firstChangedFrom = blocks[firstChangedBlockIdx]?.from ?? 0;
|
|
303
|
+
const oldPages = this.lastResult.pages;
|
|
304
|
+
let affectedPageIdx = oldPages.length;
|
|
305
|
+
for (let p = 0; p < oldPages.length; p++) {
|
|
306
|
+
const lastBlock = oldPages[p].blocks[oldPages[p].blocks.length - 1];
|
|
307
|
+
if (lastBlock && lastBlock.to >= firstChangedFrom) {
|
|
308
|
+
affectedPageIdx = p;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (affectedPageIdx > 0 && affectedPageIdx < allPages.length && oldPages[affectedPageIdx - 1]?.to === allPages[affectedPageIdx - 1]?.to) {
|
|
313
|
+
pages = [...oldPages.slice(0, affectedPageIdx), ...allPages.slice(affectedPageIdx)];
|
|
314
|
+
} else {
|
|
315
|
+
pages = allPages;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
pages = allPages;
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
pages = allPages;
|
|
322
|
+
}
|
|
286
323
|
const result = { pages, invalidatedAt: Date.now() };
|
|
287
324
|
this.lastResult = result;
|
|
288
325
|
this.lastDocHash = docHash;
|
|
326
|
+
this.lastBlockHashes = newBlockHashes;
|
|
289
327
|
this.lastSize = size;
|
|
290
328
|
this.resolveAll(result);
|
|
291
329
|
return result;
|
|
@@ -305,6 +343,18 @@ var PageLayout = class {
|
|
|
305
343
|
}
|
|
306
344
|
return hash;
|
|
307
345
|
}
|
|
346
|
+
/** Compute per-block hashes for incremental recalculation. */
|
|
347
|
+
computeBlockHashes() {
|
|
348
|
+
const map = /* @__PURE__ */ new Map();
|
|
349
|
+
let idx = 0;
|
|
350
|
+
for (const child of Array.from(this.element.children)) {
|
|
351
|
+
if (child instanceof HTMLElement) {
|
|
352
|
+
map.set(idx, child.tagName + (child.getAttribute("data-from") ?? "") + (child.getAttribute("data-to") ?? "") + (child.getAttribute("data-node-type") ?? "") + (child.textContent ?? ""));
|
|
353
|
+
}
|
|
354
|
+
idx++;
|
|
355
|
+
}
|
|
356
|
+
return map;
|
|
357
|
+
}
|
|
308
358
|
getContainerSize() {
|
|
309
359
|
const rect = this.element.getBoundingClientRect();
|
|
310
360
|
return { width: rect.width, height: rect.height };
|
|
@@ -378,9 +428,135 @@ var PageLayout = class {
|
|
|
378
428
|
return null;
|
|
379
429
|
}
|
|
380
430
|
};
|
|
431
|
+
|
|
432
|
+
// src/VirtualPageOverlay.ts
|
|
433
|
+
var VirtualPageOverlay = class {
|
|
434
|
+
pageLayout;
|
|
435
|
+
config;
|
|
436
|
+
scrollEl;
|
|
437
|
+
editorEl;
|
|
438
|
+
bufferPages;
|
|
439
|
+
lastResult = null;
|
|
440
|
+
lastVisible = [];
|
|
441
|
+
lastScrollTop = -1;
|
|
442
|
+
scrollHandler = null;
|
|
443
|
+
rafId = 0;
|
|
444
|
+
constructor(editor, config, scrollEl, bufferPages = 2) {
|
|
445
|
+
const ps = typeof config.pageSize === "string" ? getPageSize(config.pageSize) ?? { id: "a4", name: "A4", pageWidth: 794, pageHeight: 1123 } : config.pageSize;
|
|
446
|
+
this.config = {
|
|
447
|
+
pageSize: ps,
|
|
448
|
+
margins: config.margins ?? { top: 20, bottom: 20, left: 50, right: 50 },
|
|
449
|
+
pageGap: config.pageGap ?? 50,
|
|
450
|
+
headerLeft: config.headerLeft ?? "",
|
|
451
|
+
headerRight: config.headerRight ?? "",
|
|
452
|
+
footerLeft: config.footerLeft ?? "",
|
|
453
|
+
footerRight: config.footerRight ?? ""
|
|
454
|
+
};
|
|
455
|
+
this.scrollEl = scrollEl;
|
|
456
|
+
this.bufferPages = bufferPages;
|
|
457
|
+
this.editorEl = editor instanceof HTMLElement ? editor : editor.dom;
|
|
458
|
+
this.pageLayout = new PageLayout(editor, {
|
|
459
|
+
pageHeight: ps.pageHeight,
|
|
460
|
+
pageWidth: ps.pageWidth,
|
|
461
|
+
margins: this.config.margins
|
|
462
|
+
}, 150);
|
|
463
|
+
}
|
|
464
|
+
/** Run or re-run layout measurement. Resolves with current page data. */
|
|
465
|
+
async layout() {
|
|
466
|
+
const result = await this.pageLayout.layout();
|
|
467
|
+
this.lastResult = result;
|
|
468
|
+
return this.computeData();
|
|
469
|
+
}
|
|
470
|
+
/** Start observing scroll events to track visible pages. */
|
|
471
|
+
observe() {
|
|
472
|
+
if (this.scrollHandler) return;
|
|
473
|
+
this.scrollHandler = () => this.onScroll();
|
|
474
|
+
this.scrollEl.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
475
|
+
this.updateVisible();
|
|
476
|
+
}
|
|
477
|
+
/** Stop observing scroll events. */
|
|
478
|
+
disconnect() {
|
|
479
|
+
if (this.scrollHandler) {
|
|
480
|
+
this.scrollEl.removeEventListener("scroll", this.scrollHandler);
|
|
481
|
+
this.scrollHandler = null;
|
|
482
|
+
}
|
|
483
|
+
if (this.rafId) {
|
|
484
|
+
cancelAnimationFrame(this.rafId);
|
|
485
|
+
this.rafId = 0;
|
|
486
|
+
}
|
|
487
|
+
this.pageLayout.destroy();
|
|
488
|
+
}
|
|
489
|
+
/** Get current page overlay data synchronously (cached). */
|
|
490
|
+
getData() {
|
|
491
|
+
return {
|
|
492
|
+
totalPages: this.lastResult?.pages.length ?? 1,
|
|
493
|
+
visiblePages: this.lastVisible,
|
|
494
|
+
config: this.config
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Update configuration (page size, margins, etc.) — triggers re-layout. */
|
|
498
|
+
async updateConfig(config) {
|
|
499
|
+
if (config.pageSize) {
|
|
500
|
+
const ps = typeof config.pageSize === "string" ? getPageSize(config.pageSize) ?? this.config.pageSize : config.pageSize;
|
|
501
|
+
this.config.pageSize = ps;
|
|
502
|
+
this.pageLayout.destroy();
|
|
503
|
+
this.pageLayout = new PageLayout(this.editorEl, {
|
|
504
|
+
pageHeight: ps.pageHeight,
|
|
505
|
+
pageWidth: ps.pageWidth,
|
|
506
|
+
margins: this.config.margins
|
|
507
|
+
}, 150);
|
|
508
|
+
}
|
|
509
|
+
if (config.margins) {
|
|
510
|
+
this.config.margins = { ...this.config.margins, ...config.margins };
|
|
511
|
+
}
|
|
512
|
+
if (config.pageGap !== void 0) this.config.pageGap = config.pageGap;
|
|
513
|
+
if (config.headerLeft !== void 0) this.config.headerLeft = config.headerLeft;
|
|
514
|
+
if (config.headerRight !== void 0) this.config.headerRight = config.headerRight;
|
|
515
|
+
if (config.footerLeft !== void 0) this.config.footerLeft = config.footerLeft;
|
|
516
|
+
if (config.footerRight !== void 0) this.config.footerRight = config.footerRight;
|
|
517
|
+
return this.layout();
|
|
518
|
+
}
|
|
519
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
520
|
+
onScroll() {
|
|
521
|
+
if (this.rafId) return;
|
|
522
|
+
this.rafId = requestAnimationFrame(() => {
|
|
523
|
+
this.rafId = 0;
|
|
524
|
+
this.updateVisible();
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async updateVisible() {
|
|
528
|
+
const st = this.scrollEl.scrollTop;
|
|
529
|
+
if (st === this.lastScrollTop) return;
|
|
530
|
+
this.lastScrollTop = st;
|
|
531
|
+
if (!this.lastResult) {
|
|
532
|
+
await this.layout();
|
|
533
|
+
}
|
|
534
|
+
this.computeData();
|
|
535
|
+
}
|
|
536
|
+
computeData() {
|
|
537
|
+
const pages = this.lastResult?.pages ?? [];
|
|
538
|
+
const totalPages = pages.length || 1;
|
|
539
|
+
const ph = this.config.pageSize.pageHeight + this.config.pageGap;
|
|
540
|
+
const viewportTop = this.scrollEl.scrollTop;
|
|
541
|
+
const viewportBottom = viewportTop + this.scrollEl.clientHeight;
|
|
542
|
+
const visible = [];
|
|
543
|
+
const startPage = Math.max(0, Math.floor(viewportTop / ph) - this.bufferPages);
|
|
544
|
+
const endPage = Math.min(totalPages, Math.ceil(viewportBottom / ph) + this.bufferPages);
|
|
545
|
+
for (let i = startPage; i < endPage; i++) {
|
|
546
|
+
visible.push({
|
|
547
|
+
index: i,
|
|
548
|
+
top: i * ph,
|
|
549
|
+
height: ph
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
this.lastVisible = visible;
|
|
553
|
+
return { totalPages, visiblePages: visible, config: this.config };
|
|
554
|
+
}
|
|
555
|
+
};
|
|
381
556
|
export {
|
|
382
557
|
PAGE_SIZES,
|
|
383
558
|
PageBreaker,
|
|
384
559
|
PageLayout,
|
|
560
|
+
VirtualPageOverlay,
|
|
385
561
|
getPageSize
|
|
386
562
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kedataindo/docflow-layout-engine",
|
|
3
|
-
"
|
|
3
|
+
"license": "UNLICENSED",
|
|
4
|
+
"version": "0.0.3",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.js",
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
],
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@tiptap/pm": "^2.11.0",
|
|
24
|
-
"@kedataindo/docflow-core": "0.0.
|
|
25
|
+
"@kedataindo/docflow-core": "0.0.4"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"happy-dom": "^20.10.6",
|