diff --git a/apps/docs/core/superdoc/events.mdx b/apps/docs/core/superdoc/events.mdx
index 8c45aff934..76d2c66347 100644
--- a/apps/docs/core/superdoc/events.mdx
+++ b/apps/docs/core/superdoc/events.mdx
@@ -337,6 +337,35 @@ superdoc.on('locked', ({ isLocked, lockedBy }) => {
```
+## Pagination events
+
+### `pagination-update`
+
+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.
+
+
+```javascript Usage
+superdoc.on('pagination-update', ({ totalPages, superdoc }) => {
+ console.log(`Document has ${totalPages} pages`);
+});
+```
+
+```javascript Full Example
+import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
+
+const superdoc = new SuperDoc({
+ selector: '#editor',
+ document: yourFile,
+ pagination: true,
+});
+
+superdoc.on('pagination-update', ({ totalPages, superdoc }) => {
+ console.log(`Document has ${totalPages} pages`);
+});
+```
+
+
## UI events
### `zoomChange`
@@ -432,6 +461,7 @@ new SuperDoc({
onEditorCreate: ({ editor }) => { },
onEditorUpdate: ({ editor }) => { },
onFontsResolved: ({ documentFonts, unsupportedFonts }) => { },
+ onPaginationUpdate: ({ totalPages, superdoc }) => { },
onSidebarToggle: (isOpened) => { },
onException: ({ error }) => { },
});
@@ -443,5 +473,6 @@ new SuperDoc({
2. `editorCreate` — Editor ready
3. `ready` — All editors ready
4. `collaboration-ready` — If collaboration enabled
-5. Runtime events (`editor-update`, `comments-update`, `sidebar-toggle`, etc.)
-6. `editorDestroy` — Cleanup
+5. `pagination-update` — After each layout pass (page count available)
+6. Runtime events (`editor-update`, `comments-update`, `sidebar-toggle`, etc.)
+7. `editorDestroy` — Cleanup
diff --git a/apps/docs/core/supereditor/methods.mdx b/apps/docs/core/supereditor/methods.mdx
index 12bf029bef..1ac5a5553f 100644
--- a/apps/docs/core/supereditor/methods.mdx
+++ b/apps/docs/core/supereditor/methods.mdx
@@ -860,4 +860,5 @@ editor.commands.goToSearchResult(results[0]);
| `isDestroyed` | `boolean` | Whether editor has been destroyed |
| `isFocused` | `boolean` | Whether editor has focus |
| `docChanged` | `boolean` | Whether any edits have been made |
+| `currentTotalPages` | `number \| undefined` | Page count after the first layout completes. `undefined` until then. Use the `pagination-update` event to know when it's available. |
| `sourcePath` | `string \| null` | Source file path (null if opened from Blob) |
diff --git a/packages/super-editor/src/core/Editor.ts b/packages/super-editor/src/core/Editor.ts
index 5122123b58..fae9ff7f51 100644
--- a/packages/super-editor/src/core/Editor.ts
+++ b/packages/super-editor/src/core/Editor.ts
@@ -224,6 +224,19 @@ export class Editor extends EventEmitter {
*/
presentationEditor: PresentationEditor | null = null;
+ /**
+ * Returns the current total number of pages when pagination is active.
+ * Delegates to the PresentationEditor's layout state.
+ * Returns `undefined` before the first layout completes or when pagination is off.
+ */
+ get currentTotalPages(): number | undefined {
+ if (this.presentationEditor) {
+ const pages = this.presentationEditor.getPages();
+ return pages.length > 0 ? pages.length : undefined;
+ }
+ return undefined;
+ }
+
/**
* Whether the editor currently has focus
*/
diff --git a/packages/super-editor/src/tests/editor/currentTotalPages.test.js b/packages/super-editor/src/tests/editor/currentTotalPages.test.js
new file mode 100644
index 0000000000..f20d9f1dcb
--- /dev/null
+++ b/packages/super-editor/src/tests/editor/currentTotalPages.test.js
@@ -0,0 +1,99 @@
+/* @vitest-environment node */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { readFile } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import { Editor } from '@core/Editor.js';
+import { getStarterExtensions } from '@extensions/index.js';
+import { createDOMGlobalsLifecycle } from '../helpers/dom-globals-test-utils.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const loadDocxFixture = async (filename) => {
+ return readFile(join(__dirname, '../data', filename));
+};
+
+describe('Editor.currentTotalPages', () => {
+ const domLifecycle = createDOMGlobalsLifecycle();
+
+ beforeEach(() => {
+ domLifecycle.setup();
+ });
+
+ afterEach(() => {
+ domLifecycle.teardown();
+ });
+
+ it('returns undefined when presentationEditor is null', async () => {
+ const buffer = await loadDocxFixture('blank-doc.docx');
+ const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
+
+ const editor = new Editor({
+ mode: 'docx',
+ documentId: 'test-no-pagination',
+ extensions: getStarterExtensions(),
+ content,
+ mediaFiles,
+ fonts,
+ });
+
+ expect(editor.presentationEditor).toBeNull();
+ expect(editor.currentTotalPages).toBeUndefined();
+
+ editor.destroy();
+ });
+
+ it('returns undefined when presentationEditor has no pages yet', async () => {
+ const buffer = await loadDocxFixture('blank-doc.docx');
+ const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
+
+ const editor = new Editor({
+ mode: 'docx',
+ documentId: 'test-empty-pages',
+ extensions: getStarterExtensions(),
+ content,
+ mediaFiles,
+ fonts,
+ });
+
+ // Simulate a presentationEditor with empty pages (before first layout)
+ editor.presentationEditor = /** @type {any} */ ({
+ getPages: vi.fn(() => []),
+ });
+
+ expect(editor.currentTotalPages).toBeUndefined();
+
+ editor.presentationEditor = null;
+ editor.destroy();
+ });
+
+ it('returns the page count when presentationEditor has pages', async () => {
+ const buffer = await loadDocxFixture('blank-doc.docx');
+ const [content, , mediaFiles, fonts] = await Editor.loadXmlData(buffer, true);
+
+ const editor = new Editor({
+ mode: 'docx',
+ documentId: 'test-with-pages',
+ extensions: getStarterExtensions(),
+ content,
+ mediaFiles,
+ fonts,
+ });
+
+ // Simulate a presentationEditor after layout completes
+ editor.presentationEditor = /** @type {any} */ ({
+ getPages: vi.fn(() => [
+ { number: 1, size: { w: 612, h: 792 } },
+ { number: 2, size: { w: 612, h: 792 } },
+ { number: 3, size: { w: 612, h: 792 } },
+ ]),
+ });
+
+ expect(editor.currentTotalPages).toBe(3);
+
+ editor.presentationEditor = null;
+ editor.destroy();
+ });
+});
diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue
index ab9d469ff4..2ae153f7ff 100644
--- a/packages/superdoc/src/SuperDoc.vue
+++ b/packages/superdoc/src/SuperDoc.vue
@@ -280,6 +280,11 @@ const onEditorReady = ({ editor, presentationEditor }) => {
hasInitializedLocations.value = true;
}
});
+
+ presentationEditor.on('paginationUpdate', ({ layout }) => {
+ const totalPages = layout.pages.length;
+ proxy.$superdoc.emit('pagination-update', { totalPages, superdoc: proxy.$superdoc });
+ });
};
const onEditorDestroy = () => {
diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js
index 20156d0a1a..c08d857032 100644
--- a/packages/superdoc/src/core/SuperDoc.js
+++ b/packages/superdoc/src/core/SuperDoc.js
@@ -122,6 +122,7 @@ export class SuperDoc extends EventEmitter {
onCommentsListChange: () => null,
onException: () => null,
onListDefinitionsChange: () => null,
+ onPaginationUpdate: () => null,
onTransaction: () => null,
onFontsResolved: null,
@@ -449,6 +450,7 @@ export class SuperDoc extends EventEmitter {
this.on('content-error', this.onContentError);
this.on('exception', this.config.onException);
this.on('list-definitions-change', this.config.onListDefinitionsChange);
+ this.on('pagination-update', this.config.onPaginationUpdate);
if (this.config.onFontsResolved) {
this.on('fonts-resolved', this.config.onFontsResolved);
diff --git a/packages/superdoc/src/core/SuperDoc.test.js b/packages/superdoc/src/core/SuperDoc.test.js
index a2f171dc26..fe2bc62175 100644
--- a/packages/superdoc/src/core/SuperDoc.test.js
+++ b/packages/superdoc/src/core/SuperDoc.test.js
@@ -1444,4 +1444,36 @@ describe('SuperDoc core', () => {
warnSpy.mockRestore();
});
});
+
+ describe('pagination-update event', () => {
+ it('registers onPaginationUpdate listener during init', async () => {
+ createAppHarness();
+ const onPaginationUpdate = vi.fn();
+
+ const instance = new SuperDoc({
+ selector: '#host',
+ document: 'https://example.com/doc.docx',
+ onPaginationUpdate,
+ });
+ await flushMicrotasks();
+
+ instance.emit('pagination-update', { totalPages: 5, superdoc: instance });
+ expect(onPaginationUpdate).toHaveBeenCalledWith({ totalPages: 5, superdoc: instance });
+ });
+
+ it('defaults onPaginationUpdate to a no-op', async () => {
+ createAppHarness();
+
+ const instance = new SuperDoc({
+ selector: '#host',
+ document: 'https://example.com/doc.docx',
+ });
+ await flushMicrotasks();
+
+ // Should not throw when emitting without a user callback
+ expect(() => {
+ instance.emit('pagination-update', { totalPages: 3, superdoc: instance });
+ }).not.toThrow();
+ });
+ });
});
diff --git a/packages/superdoc/src/core/types/index.js b/packages/superdoc/src/core/types/index.js
index 13926dc3e6..f0adbeec4e 100644
--- a/packages/superdoc/src/core/types/index.js
+++ b/packages/superdoc/src/core/types/index.js
@@ -184,6 +184,7 @@
* @property {(params: { editor: Editor }) => void} [onEditorUpdate] Callback when document is updated
* @property {(params: { error: Error }) => void} [onException] Callback when an exception is thrown
* @property {(params: { isRendered: boolean }) => void} [onCommentsListChange] Callback when the comments list is rendered
+ * @property {(params: { totalPages: number, superdoc: SuperDoc }) => void} [onPaginationUpdate] Callback when pagination layout updates (fires after each layout pass with the current page count)
* @property {(params: {})} [onListDefinitionsChange] Callback when the list definitions change
* @property {string} [format] The format of the document (docx, pdf, html)
* @property {Object[]} [editorExtensions] The extensions to load for the editor