Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5402,21 +5402,42 @@ export class PresentationEditor extends EventEmitter {
*/
#applyZoom() {
if (this.#isSemanticFlowMode()) {
// Semantic mode: fill the container with fluid widths, no zoom scaling.
this.#viewportHost.style.width = '100%';
const zoom = this.#layoutOptions.zoom ?? 1;

// Semantic mode: fluid widths with optional zoom scaling.
this.#viewportHost.style.minWidth = '';
this.#viewportHost.style.minHeight = '';
this.#viewportHost.style.transform = '';

this.#painterHost.style.width = '100%';
this.#painterHost.style.minHeight = '';
this.#painterHost.style.transformOrigin = '';
this.#painterHost.style.transform = '';
if (zoom === 1) {
this.#viewportHost.style.width = '100%';
this.#viewportHost.style.transform = '';

this.#selectionOverlay.style.width = '100%';
this.#selectionOverlay.style.height = '100%';
this.#selectionOverlay.style.transformOrigin = '';
this.#selectionOverlay.style.transform = '';
this.#painterHost.style.width = '100%';
this.#painterHost.style.minHeight = '';
this.#painterHost.style.transformOrigin = '';
this.#painterHost.style.transform = '';

this.#selectionOverlay.style.width = '100%';
this.#selectionOverlay.style.height = '100%';
this.#selectionOverlay.style.transformOrigin = '';
this.#selectionOverlay.style.transform = '';
} else {
// Scale content while keeping fluid layout: set unscaled width to
// container/zoom so the reflowed content visually fills the container
// after the CSS transform enlarges it.
this.#viewportHost.style.width = `${100 / zoom}%`;
this.#viewportHost.style.transform = '';

this.#painterHost.style.width = '100%';
this.#painterHost.style.minHeight = '';
this.#painterHost.style.transformOrigin = 'top left';
this.#painterHost.style.transform = `scale(${zoom})`;

this.#selectionOverlay.style.width = '100%';
this.#selectionOverlay.style.height = '100%';
this.#selectionOverlay.style.transformOrigin = 'top left';
this.#selectionOverlay.style.transform = `scale(${zoom})`;
}
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -983,4 +983,78 @@ describe('PresentationEditor - Zoom Functionality', () => {
}
});
});

describe('semantic flow mode zoom', () => {
let semanticEditor: PresentationEditor;

afterEach(() => {
if (semanticEditor) {
semanticEditor.destroy();
}
});

it('should apply CSS transform when zoom is set in semantic mode', () => {
semanticEditor = new PresentationEditor({
element: container,
documentId: 'test-doc-semantic-zoom',
pageSize: { w: 612, h: 792 },
layoutEngineOptions: { flowMode: 'semantic' },
});

semanticEditor.setZoom(1.5);

const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement;
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;

expect(painterHost?.style.transform).toBe('scale(1.5)');
expect(painterHost?.style.transformOrigin).toBe('top left');

expect(selectionOverlay?.style.transform).toBe('scale(1.5)');
expect(selectionOverlay?.style.transformOrigin).toBe('top left');

// Viewport width should be narrowed to compensate for scale
expect(viewportHost?.style.width).toBe(`${100 / 1.5}%`);
});

it('should clear transforms when zoom is reset to 1 in semantic mode', () => {
semanticEditor = new PresentationEditor({
element: container,
documentId: 'test-doc-semantic-zoom-reset',
pageSize: { w: 612, h: 792 },
layoutEngineOptions: { flowMode: 'semantic' },
});

semanticEditor.setZoom(2);
semanticEditor.setZoom(1);

const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement;
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;

expect(painterHost?.style.transform).toBe('');
expect(painterHost?.style.transformOrigin).toBe('');
expect(selectionOverlay?.style.transform).toBe('');
expect(selectionOverlay?.style.transformOrigin).toBe('');
expect(viewportHost?.style.width).toBe('100%');
});

it('should keep fluid width on all elements in semantic mode', () => {
semanticEditor = new PresentationEditor({
element: container,
documentId: 'test-doc-semantic-zoom-fluid',
pageSize: { w: 612, h: 792 },
layoutEngineOptions: { flowMode: 'semantic' },
});

semanticEditor.setZoom(0.75);

const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;

// painterHost and selectionOverlay keep 100% width (viewport is narrowed instead)
expect(painterHost?.style.width).toBe('100%');
expect(selectionOverlay?.style.width).toBe('100%');
});
});
});
24 changes: 22 additions & 2 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1255,12 +1255,32 @@ const handlePdfSelectionRaw = ({ selectionBounds, documentId, page }) => {
watch(
() => activeZoom.value,
(zoom) => {
const zoomFactor = (zoom ?? 100) / 100;

if (proxy.$superdoc.config.useLayoutEngine !== false) {
PresentationEditor.setGlobalZoom((zoom ?? 100) / 100);
PresentationEditor.setGlobalZoom(zoomFactor);
} else {
// Web layout without layout engine — apply CSS transform directly
// to non-PDF sub-document containers so zoom works for PM fallback rendering.
// PDF documents are excluded because pdfViewer.updateScale() handles their zoom
// separately below; applying both would result in double-zoom.
const subDocs = layers.value?.querySelectorAll('.superdoc__sub-document');
Comment thread
andrii-harbour marked this conversation as resolved.
subDocs?.forEach((el) => {
if (el.querySelector('.sd-pdf-viewer')) return;
if (zoomFactor === 1) {
el.style.transformOrigin = '';
el.style.transform = '';
el.style.width = '';
} else {
el.style.transformOrigin = 'top left';
el.style.transform = `scale(${zoomFactor})`;
el.style.width = `${100 / zoomFactor}%`;
}
});
}

const pdfViewer = getPDFViewer();
pdfViewer?.updateScale((zoom ?? 100) / 100);
pdfViewer?.updateScale(zoomFactor);

nextTick(() => {
updateWhiteboardPageSizes();
Expand Down
Loading