Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/superdoc/src/core/SuperDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
276 changes: 276 additions & 0 deletions packages/superdoc/src/core/SuperDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
});