diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index c6abffa09..5639db8e8 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -816,6 +816,49 @@ export class SuperDoc extends EventEmitter { return this.activeEditor?.commands.goToSearchResult(match); } + /** + * Get the current zoom level as a percentage (e.g., 100 for 100%) + * @returns {number} The current zoom level as a percentage + * @example + * const zoom = superdoc.getZoom(); // Returns 100, 150, 200, etc. + */ + getZoom() { + const doc = this.superdocStore?.documents?.[0]; + const presentationEditor = typeof doc?.getPresentationEditor === 'function' ? doc.getPresentationEditor() : null; + if (presentationEditor && typeof presentationEditor.zoom === 'number') { + return Math.round(presentationEditor.zoom * 100); + } + // Fallback to 100% if no presentation editor + return 100; + } + + /** + * Set the zoom level for all documents + * @param {number} percent - The zoom level as a percentage (e.g., 100, 150, 200) + * @example + * superdoc.setZoom(150); // Set zoom to 150% + * superdoc.setZoom(50); // Set zoom to 50% + */ + setZoom(percent) { + if (typeof percent !== 'number' || !Number.isFinite(percent) || percent <= 0) { + console.warn('[SuperDoc] setZoom expects a positive number representing percentage'); + return; + } + + const zoomMultiplier = percent / 100; + + // Update all presentation editors + this.superdocStore?.documents?.forEach((doc) => { + const presentationEditor = typeof doc?.getPresentationEditor === 'function' ? doc.getPresentationEditor() : null; + if (presentationEditor && typeof presentationEditor.setZoom === 'function') { + presentationEditor.setZoom(zoomMultiplier); + } + }); + + // Emit zoom change event + this.emit('zoomChange', { zoom: percent }); + } + /** * Set the document to locked or unlocked * @param {boolean} lock diff --git a/packages/superdoc/src/core/SuperDoc.test.js b/packages/superdoc/src/core/SuperDoc.test.js index 3b03ff1d5..0810099f3 100644 --- a/packages/superdoc/src/core/SuperDoc.test.js +++ b/packages/superdoc/src/core/SuperDoc.test.js @@ -792,4 +792,280 @@ describe('SuperDoc core', () => { }); }); }); + + describe('Zoom API', () => { + it('getZoom returns 100 when no presentation editor is available', async () => { + const { superdocStore } = createAppHarness(); + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => null), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + expect(instance.getZoom()).toBe(100); + }); + + it('getZoom returns correct percentage from presentation editor', async () => { + const { superdocStore } = createAppHarness(); + const mockPresentationEditor = { + zoom: 1.5, // 150% + setZoom: vi.fn(), + }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + expect(instance.getZoom()).toBe(150); + }); + + it('getZoom rounds to nearest integer', async () => { + const { superdocStore } = createAppHarness(); + const mockPresentationEditor = { + zoom: 0.333, // 33.3% + setZoom: vi.fn(), + }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + expect(instance.getZoom()).toBe(33); + }); + + it('setZoom calls presentation editor setZoom with correct multiplier', async () => { + const { superdocStore } = createAppHarness(); + const mockPresentationEditor = { + zoom: 1, + setZoom: vi.fn(), + }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + instance.setZoom(150); + + expect(mockPresentationEditor.setZoom).toHaveBeenCalledWith(1.5); + }); + + it('setZoom emits zoomChange event', async () => { + const { superdocStore } = createAppHarness(); + const mockPresentationEditor = { + zoom: 1, + setZoom: vi.fn(), + }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const zoomChangeSpy = vi.fn(); + instance.on('zoomChange', zoomChangeSpy); + + instance.setZoom(200); + + expect(zoomChangeSpy).toHaveBeenCalledWith({ zoom: 200 }); + }); + + it('setZoom updates all presentation editors in multi-document mode', async () => { + const { superdocStore } = createAppHarness(); + const mockPresentationEditor1 = { zoom: 1, setZoom: vi.fn() }; + const mockPresentationEditor2 = { zoom: 1, setZoom: vi.fn() }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor1), + }, + { + id: 'doc-2', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor2), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + instance.setZoom(75); + + expect(mockPresentationEditor1.setZoom).toHaveBeenCalledWith(0.75); + expect(mockPresentationEditor2.setZoom).toHaveBeenCalledWith(0.75); + }); + + it('setZoom warns and returns early for invalid values', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { superdocStore } = createAppHarness(); + const mockPresentationEditor = { zoom: 1, setZoom: vi.fn() }; + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => mockPresentationEditor), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const zoomChangeSpy = vi.fn(); + instance.on('zoomChange', zoomChangeSpy); + + // Test negative value + instance.setZoom(-50); + expect(warnSpy).toHaveBeenCalledWith('[SuperDoc] setZoom expects a positive number representing percentage'); + expect(mockPresentationEditor.setZoom).not.toHaveBeenCalled(); + expect(zoomChangeSpy).not.toHaveBeenCalled(); + + warnSpy.mockClear(); + mockPresentationEditor.setZoom.mockClear(); + + // Test zero + instance.setZoom(0); + expect(warnSpy).toHaveBeenCalled(); + expect(mockPresentationEditor.setZoom).not.toHaveBeenCalled(); + + warnSpy.mockClear(); + mockPresentationEditor.setZoom.mockClear(); + + // Test non-number + instance.setZoom('150'); + expect(warnSpy).toHaveBeenCalled(); + expect(mockPresentationEditor.setZoom).not.toHaveBeenCalled(); + + warnSpy.mockClear(); + mockPresentationEditor.setZoom.mockClear(); + + // Test NaN + instance.setZoom(NaN); + expect(warnSpy).toHaveBeenCalled(); + expect(mockPresentationEditor.setZoom).not.toHaveBeenCalled(); + + warnSpy.mockClear(); + mockPresentationEditor.setZoom.mockClear(); + + // Test Infinity + instance.setZoom(Infinity); + expect(warnSpy).toHaveBeenCalled(); + expect(mockPresentationEditor.setZoom).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('setZoom handles documents without presentation editor gracefully', async () => { + const { superdocStore } = createAppHarness(); + + superdocStore.documents = [ + { + id: 'doc-1', + type: DOCX, + getPresentationEditor: vi.fn(() => null), + }, + ]; + + const instance = new SuperDoc({ + selector: '#host', + document: 'https://example.com/doc.docx', + documents: [], + modules: { comments: {}, toolbar: {} }, + colors: ['red'], + user: { name: 'Jane', email: 'jane@example.com' }, + }); + await flushMicrotasks(); + + const zoomChangeSpy = vi.fn(); + instance.on('zoomChange', zoomChangeSpy); + + // Should not throw + expect(() => instance.setZoom(150)).not.toThrow(); + + // Event should still be emitted + expect(zoomChangeSpy).toHaveBeenCalledWith({ zoom: 150 }); + }); + }); });