Skip to content

Commit 95b4579

Browse files
authored
fix: add currentTotalPages getter and pagination-update event (#2202)
* fix: add currentTotalPages getter and pagination-update event - Add `currentTotalPages` getter on Editor that delegates to PresentationEditor.getPages(), making `activeEditor.currentTotalPages` return the page count after layout completes. - Bridge PresentationEditor's `paginationUpdate` to a SuperDoc-level `pagination-update` event with `onPaginationUpdate` config callback, so consumers can react when page data becomes available. - Add tests for both the getter (3 cases) and the event registration. Closes #958 * fix(superdoc): remove unnecessary optional chaining in pagination-update handler `layout.pages` is always present when `paginationUpdate` fires (the Layout type defines `pages: Page[]` as required, and PresentationEditor guards against invalid layout results before emitting). The `?.` and `?? 0` were dead code. * docs(events): document pagination-update event Add the new `pagination-update` event to the events reference page with usage examples, configuration callback, and event order. * docs(supereditor): document currentTotalPages property Add `currentTotalPages` to the editor properties table with a note that it returns `undefined` until the first layout pass completes.
1 parent 6a5a2e6 commit 95b4579

8 files changed

Lines changed: 186 additions & 2 deletions

File tree

apps/docs/core/superdoc/events.mdx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,35 @@ superdoc.on('locked', ({ isLocked, lockedBy }) => {
337337
```
338338
</CodeGroup>
339339

340+
## Pagination events
341+
342+
### `pagination-update`
343+
344+
Fired after each layout pass with the current page count. Use this to know when page data is available — `activeEditor.currentTotalPages` is only populated after the first layout completes, which happens *after* the `ready` event.
345+
346+
<CodeGroup>
347+
```javascript Usage
348+
superdoc.on('pagination-update', ({ totalPages, superdoc }) => {
349+
console.log(`Document has ${totalPages} pages`);
350+
});
351+
```
352+
353+
```javascript Full Example
354+
import { SuperDoc } from 'superdoc';
355+
import 'superdoc/style.css';
356+
357+
const superdoc = new SuperDoc({
358+
selector: '#editor',
359+
document: yourFile,
360+
pagination: true,
361+
});
362+
363+
superdoc.on('pagination-update', ({ totalPages, superdoc }) => {
364+
console.log(`Document has ${totalPages} pages`);
365+
});
366+
```
367+
</CodeGroup>
368+
340369
## UI events
341370

342371
### `zoomChange`
@@ -432,6 +461,7 @@ new SuperDoc({
432461
onEditorCreate: ({ editor }) => { },
433462
onEditorUpdate: ({ editor }) => { },
434463
onFontsResolved: ({ documentFonts, unsupportedFonts }) => { },
464+
onPaginationUpdate: ({ totalPages, superdoc }) => { },
435465
onSidebarToggle: (isOpened) => { },
436466
onException: ({ error }) => { },
437467
});
@@ -443,5 +473,6 @@ new SuperDoc({
443473
2. `editorCreate` — Editor ready
444474
3. `ready` — All editors ready
445475
4. `collaboration-ready` — If collaboration enabled
446-
5. Runtime events (`editor-update`, `comments-update`, `sidebar-toggle`, etc.)
447-
6. `editorDestroy` — Cleanup
476+
5. `pagination-update` — After each layout pass (page count available)
477+
6. Runtime events (`editor-update`, `comments-update`, `sidebar-toggle`, etc.)
478+
7. `editorDestroy` — Cleanup

apps/docs/core/supereditor/methods.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,4 +860,5 @@ editor.commands.goToSearchResult(results[0]);
860860
| `isDestroyed` | `boolean` | Whether editor has been destroyed |
861861
| `isFocused` | `boolean` | Whether editor has focus |
862862
| `docChanged` | `boolean` | Whether any edits have been made |
863+
| `currentTotalPages` | `number \| undefined` | Page count after the first layout completes. `undefined` until then. Use the `pagination-update` event to know when it's available. |
863864
| `sourcePath` | `string \| null` | Source file path (null if opened from Blob) |

packages/super-editor/src/core/Editor.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,19 @@ export class Editor extends EventEmitter<EditorEventMap> {
224224
*/
225225
presentationEditor: PresentationEditor | null = null;
226226

227+
/**
228+
* Returns the current total number of pages when pagination is active.
229+
* Delegates to the PresentationEditor's layout state.
230+
* Returns `undefined` before the first layout completes or when pagination is off.
231+
*/
232+
get currentTotalPages(): number | undefined {
233+
if (this.presentationEditor) {
234+
const pages = this.presentationEditor.getPages();
235+
return pages.length > 0 ? pages.length : undefined;
236+
}
237+
return undefined;
238+
}
239+
227240
/**
228241
* Whether the editor currently has focus
229242
*/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* @vitest-environment node */
2+
3+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
4+
import { readFile } from 'node:fs/promises';
5+
import { fileURLToPath } from 'node:url';
6+
import { dirname, join } from 'node:path';
7+
import { Editor } from '@core/Editor.js';
8+
import { getStarterExtensions } from '@extensions/index.js';
9+
import { createDOMGlobalsLifecycle } from '../helpers/dom-globals-test-utils.js';
10+
11+
const __filename = fileURLToPath(import.meta.url);
12+
const __dirname = dirname(__filename);
13+
14+
const loadDocxFixture = async (filename) => {
15+
return readFile(join(__dirname, '../data', filename));
16+
};
17+
18+
describe('Editor.currentTotalPages', () => {
19+
const domLifecycle = createDOMGlobalsLifecycle();
20+
21+
beforeEach(() => {
22+
domLifecycle.setup();
23+
});
24+
25+
afterEach(() => {
26+
domLifecycle.teardown();
27+
});
28+
29+
it('returns undefined when presentationEditor is null', async () => {
30+
const buffer = await loadDocxFixture('blank-doc.docx');
31+
const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
32+
33+
const editor = new Editor({
34+
mode: 'docx',
35+
documentId: 'test-no-pagination',
36+
extensions: getStarterExtensions(),
37+
content,
38+
mediaFiles,
39+
fonts,
40+
});
41+
42+
expect(editor.presentationEditor).toBeNull();
43+
expect(editor.currentTotalPages).toBeUndefined();
44+
45+
editor.destroy();
46+
});
47+
48+
it('returns undefined when presentationEditor has no pages yet', async () => {
49+
const buffer = await loadDocxFixture('blank-doc.docx');
50+
const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
51+
52+
const editor = new Editor({
53+
mode: 'docx',
54+
documentId: 'test-empty-pages',
55+
extensions: getStarterExtensions(),
56+
content,
57+
mediaFiles,
58+
fonts,
59+
});
60+
61+
// Simulate a presentationEditor with empty pages (before first layout)
62+
editor.presentationEditor = /** @type {any} */ ({
63+
getPages: vi.fn(() => []),
64+
});
65+
66+
expect(editor.currentTotalPages).toBeUndefined();
67+
68+
editor.presentationEditor = null;
69+
editor.destroy();
70+
});
71+
72+
it('returns the page count when presentationEditor has pages', async () => {
73+
const buffer = await loadDocxFixture('blank-doc.docx');
74+
const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
75+
76+
const editor = new Editor({
77+
mode: 'docx',
78+
documentId: 'test-with-pages',
79+
extensions: getStarterExtensions(),
80+
content,
81+
mediaFiles,
82+
fonts,
83+
});
84+
85+
// Simulate a presentationEditor after layout completes
86+
editor.presentationEditor = /** @type {any} */ ({
87+
getPages: vi.fn(() => [
88+
{ number: 1, size: { w: 612, h: 792 } },
89+
{ number: 2, size: { w: 612, h: 792 } },
90+
{ number: 3, size: { w: 612, h: 792 } },
91+
]),
92+
});
93+
94+
expect(editor.currentTotalPages).toBe(3);
95+
96+
editor.presentationEditor = null;
97+
editor.destroy();
98+
});
99+
});

packages/superdoc/src/SuperDoc.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@ const onEditorReady = ({ editor, presentationEditor }) => {
280280
hasInitializedLocations.value = true;
281281
}
282282
});
283+
284+
presentationEditor.on('paginationUpdate', ({ layout }) => {
285+
const totalPages = layout.pages.length;
286+
proxy.$superdoc.emit('pagination-update', { totalPages, superdoc: proxy.$superdoc });
287+
});
283288
};
284289
285290
const onEditorDestroy = () => {

packages/superdoc/src/core/SuperDoc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export class SuperDoc extends EventEmitter {
122122
onCommentsListChange: () => null,
123123
onException: () => null,
124124
onListDefinitionsChange: () => null,
125+
onPaginationUpdate: () => null,
125126
onTransaction: () => null,
126127
onFontsResolved: null,
127128

@@ -449,6 +450,7 @@ export class SuperDoc extends EventEmitter {
449450
this.on('content-error', this.onContentError);
450451
this.on('exception', this.config.onException);
451452
this.on('list-definitions-change', this.config.onListDefinitionsChange);
453+
this.on('pagination-update', this.config.onPaginationUpdate);
452454

453455
if (this.config.onFontsResolved) {
454456
this.on('fonts-resolved', this.config.onFontsResolved);

packages/superdoc/src/core/SuperDoc.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,4 +1444,36 @@ describe('SuperDoc core', () => {
14441444
warnSpy.mockRestore();
14451445
});
14461446
});
1447+
1448+
describe('pagination-update event', () => {
1449+
it('registers onPaginationUpdate listener during init', async () => {
1450+
createAppHarness();
1451+
const onPaginationUpdate = vi.fn();
1452+
1453+
const instance = new SuperDoc({
1454+
selector: '#host',
1455+
document: 'https://example.com/doc.docx',
1456+
onPaginationUpdate,
1457+
});
1458+
await flushMicrotasks();
1459+
1460+
instance.emit('pagination-update', { totalPages: 5, superdoc: instance });
1461+
expect(onPaginationUpdate).toHaveBeenCalledWith({ totalPages: 5, superdoc: instance });
1462+
});
1463+
1464+
it('defaults onPaginationUpdate to a no-op', async () => {
1465+
createAppHarness();
1466+
1467+
const instance = new SuperDoc({
1468+
selector: '#host',
1469+
document: 'https://example.com/doc.docx',
1470+
});
1471+
await flushMicrotasks();
1472+
1473+
// Should not throw when emitting without a user callback
1474+
expect(() => {
1475+
instance.emit('pagination-update', { totalPages: 3, superdoc: instance });
1476+
}).not.toThrow();
1477+
});
1478+
});
14471479
});

packages/superdoc/src/core/types/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
* @property {(params: { editor: Editor }) => void} [onEditorUpdate] Callback when document is updated
185185
* @property {(params: { error: Error }) => void} [onException] Callback when an exception is thrown
186186
* @property {(params: { isRendered: boolean }) => void} [onCommentsListChange] Callback when the comments list is rendered
187+
* @property {(params: { totalPages: number, superdoc: SuperDoc }) => void} [onPaginationUpdate] Callback when pagination layout updates (fires after each layout pass with the current page count)
187188
* @property {(params: {})} [onListDefinitionsChange] Callback when the list definitions change
188189
* @property {string} [format] The format of the document (docx, pdf, html)
189190
* @property {Object[]} [editorExtensions] The extensions to load for the editor

0 commit comments

Comments
 (0)