@kedataindo/docflow-layout-engine 0.0.1 → 0.0.2
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/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 +9 -8
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.2",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"module": "./dist/index.js",
|
|
@@ -19,19 +20,19 @@
|
|
|
19
20
|
"files": [
|
|
20
21
|
"dist"
|
|
21
22
|
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test:unit": "vitest run"
|
|
27
|
+
},
|
|
22
28
|
"dependencies": {
|
|
23
29
|
"@tiptap/pm": "^2.11.0",
|
|
24
|
-
"@kedataindo/docflow-core": "0.0.
|
|
30
|
+
"@kedataindo/docflow-core": "0.0.3"
|
|
25
31
|
},
|
|
26
32
|
"devDependencies": {
|
|
27
33
|
"happy-dom": "^20.10.6",
|
|
28
34
|
"tsup": "^8.3.0",
|
|
29
35
|
"typescript": "^5.6.3",
|
|
30
36
|
"vitest": "^2.1.3"
|
|
31
|
-
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
34
|
-
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test:unit": "vitest run"
|
|
36
37
|
}
|
|
37
|
-
}
|
|
38
|
+
}
|