From 86cc605123d966d7bf8b3abad567c4aee6f31bd0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 27 Feb 2026 02:41:23 -0500 Subject: [PATCH 1/4] 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 --- packages/super-editor/src/core/Editor.ts | 13 +++ .../tests/editor/currentTotalPages.test.js | 99 +++++++++++++++++++ packages/superdoc/src/SuperDoc.vue | 5 + packages/superdoc/src/core/SuperDoc.js | 2 + packages/superdoc/src/core/SuperDoc.test.js | 32 ++++++ packages/superdoc/src/core/types/index.js | 1 + 6 files changed, 152 insertions(+) create mode 100644 packages/super-editor/src/tests/editor/currentTotalPages.test.js 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..c2f1fad166 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 ?? 0; + 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 From 8a9b48e7bb0d81814f75c473474ab3d655516424 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 27 Feb 2026 10:10:26 -0500 Subject: [PATCH 2/4] 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. --- packages/superdoc/src/SuperDoc.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index c2f1fad166..2ae153f7ff 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -282,7 +282,7 @@ const onEditorReady = ({ editor, presentationEditor }) => { }); presentationEditor.on('paginationUpdate', ({ layout }) => { - const totalPages = layout?.pages?.length ?? 0; + const totalPages = layout.pages.length; proxy.$superdoc.emit('pagination-update', { totalPages, superdoc: proxy.$superdoc }); }); }; From 8483b47f4a9173d70b6afa62170905c7b4829ab4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 27 Feb 2026 10:22:20 -0500 Subject: [PATCH 3/4] 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. --- apps/docs/core/superdoc/events.mdx | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) 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 From 16cc4c6f176f7a0e682690919638e84a43028e51 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 27 Feb 2026 10:27:36 -0500 Subject: [PATCH 4/4] 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. --- apps/docs/core/supereditor/methods.mdx | 1 + 1 file changed, 1 insertion(+) 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) |