Skip to content

Commit d44de69

Browse files
feat(presentation-editor): enhance zoom functionality in web layout (#2408)
* feat(presentation-editor): enhance zoom functionality in semantic flow mode - Updated the #applyZoom method to support optional zoom scaling while maintaining fluid widths in semantic mode. - Added CSS transformations for painterHost and selectionOverlay based on the zoom level. - Introduced tests to verify correct application of zoom settings and transformations in semantic flow mode, ensuring proper behavior when zoom is adjusted or reset. * fix: refine zoom handling for non-PDF documents
1 parent 0bb9289 commit d44de69

3 files changed

Lines changed: 128 additions & 13 deletions

File tree

packages/super-editor/src/core/presentation-editor/PresentationEditor.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5448,21 +5448,42 @@ export class PresentationEditor extends EventEmitter {
54485448
*/
54495449
#applyZoom() {
54505450
if (this.#isSemanticFlowMode()) {
5451-
// Semantic mode: fill the container with fluid widths, no zoom scaling.
5452-
this.#viewportHost.style.width = '100%';
5451+
const zoom = this.#layoutOptions.zoom ?? 1;
5452+
5453+
// Semantic mode: fluid widths with optional zoom scaling.
54535454
this.#viewportHost.style.minWidth = '';
54545455
this.#viewportHost.style.minHeight = '';
5455-
this.#viewportHost.style.transform = '';
54565456

5457-
this.#painterHost.style.width = '100%';
5458-
this.#painterHost.style.minHeight = '';
5459-
this.#painterHost.style.transformOrigin = '';
5460-
this.#painterHost.style.transform = '';
5457+
if (zoom === 1) {
5458+
this.#viewportHost.style.width = '100%';
5459+
this.#viewportHost.style.transform = '';
54615460

5462-
this.#selectionOverlay.style.width = '100%';
5463-
this.#selectionOverlay.style.height = '100%';
5464-
this.#selectionOverlay.style.transformOrigin = '';
5465-
this.#selectionOverlay.style.transform = '';
5461+
this.#painterHost.style.width = '100%';
5462+
this.#painterHost.style.minHeight = '';
5463+
this.#painterHost.style.transformOrigin = '';
5464+
this.#painterHost.style.transform = '';
5465+
5466+
this.#selectionOverlay.style.width = '100%';
5467+
this.#selectionOverlay.style.height = '100%';
5468+
this.#selectionOverlay.style.transformOrigin = '';
5469+
this.#selectionOverlay.style.transform = '';
5470+
} else {
5471+
// Scale content while keeping fluid layout: set unscaled width to
5472+
// container/zoom so the reflowed content visually fills the container
5473+
// after the CSS transform enlarges it.
5474+
this.#viewportHost.style.width = `${100 / zoom}%`;
5475+
this.#viewportHost.style.transform = '';
5476+
5477+
this.#painterHost.style.width = '100%';
5478+
this.#painterHost.style.minHeight = '';
5479+
this.#painterHost.style.transformOrigin = 'top left';
5480+
this.#painterHost.style.transform = `scale(${zoom})`;
5481+
5482+
this.#selectionOverlay.style.width = '100%';
5483+
this.#selectionOverlay.style.height = '100%';
5484+
this.#selectionOverlay.style.transformOrigin = 'top left';
5485+
this.#selectionOverlay.style.transform = `scale(${zoom})`;
5486+
}
54665487
return;
54675488
}
54685489

packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.zoom.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,4 +983,78 @@ describe('PresentationEditor - Zoom Functionality', () => {
983983
}
984984
});
985985
});
986+
987+
describe('semantic flow mode zoom', () => {
988+
let semanticEditor: PresentationEditor;
989+
990+
afterEach(() => {
991+
if (semanticEditor) {
992+
semanticEditor.destroy();
993+
}
994+
});
995+
996+
it('should apply CSS transform when zoom is set in semantic mode', () => {
997+
semanticEditor = new PresentationEditor({
998+
element: container,
999+
documentId: 'test-doc-semantic-zoom',
1000+
pageSize: { w: 612, h: 792 },
1001+
layoutEngineOptions: { flowMode: 'semantic' },
1002+
});
1003+
1004+
semanticEditor.setZoom(1.5);
1005+
1006+
const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
1007+
const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement;
1008+
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;
1009+
1010+
expect(painterHost?.style.transform).toBe('scale(1.5)');
1011+
expect(painterHost?.style.transformOrigin).toBe('top left');
1012+
1013+
expect(selectionOverlay?.style.transform).toBe('scale(1.5)');
1014+
expect(selectionOverlay?.style.transformOrigin).toBe('top left');
1015+
1016+
// Viewport width should be narrowed to compensate for scale
1017+
expect(viewportHost?.style.width).toBe(`${100 / 1.5}%`);
1018+
});
1019+
1020+
it('should clear transforms when zoom is reset to 1 in semantic mode', () => {
1021+
semanticEditor = new PresentationEditor({
1022+
element: container,
1023+
documentId: 'test-doc-semantic-zoom-reset',
1024+
pageSize: { w: 612, h: 792 },
1025+
layoutEngineOptions: { flowMode: 'semantic' },
1026+
});
1027+
1028+
semanticEditor.setZoom(2);
1029+
semanticEditor.setZoom(1);
1030+
1031+
const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
1032+
const viewportHost = container.querySelector('.presentation-editor__viewport') as HTMLElement;
1033+
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;
1034+
1035+
expect(painterHost?.style.transform).toBe('');
1036+
expect(painterHost?.style.transformOrigin).toBe('');
1037+
expect(selectionOverlay?.style.transform).toBe('');
1038+
expect(selectionOverlay?.style.transformOrigin).toBe('');
1039+
expect(viewportHost?.style.width).toBe('100%');
1040+
});
1041+
1042+
it('should keep fluid width on all elements in semantic mode', () => {
1043+
semanticEditor = new PresentationEditor({
1044+
element: container,
1045+
documentId: 'test-doc-semantic-zoom-fluid',
1046+
pageSize: { w: 612, h: 792 },
1047+
layoutEngineOptions: { flowMode: 'semantic' },
1048+
});
1049+
1050+
semanticEditor.setZoom(0.75);
1051+
1052+
const painterHost = container.querySelector('.presentation-editor__pages') as HTMLElement;
1053+
const selectionOverlay = container.querySelector('.presentation-editor__selection-overlay') as HTMLElement;
1054+
1055+
// painterHost and selectionOverlay keep 100% width (viewport is narrowed instead)
1056+
expect(painterHost?.style.width).toBe('100%');
1057+
expect(selectionOverlay?.style.width).toBe('100%');
1058+
});
1059+
});
9861060
});

packages/superdoc/src/SuperDoc.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,12 +1266,32 @@ const handlePdfSelectionRaw = ({ selectionBounds, documentId, page }) => {
12661266
watch(
12671267
() => activeZoom.value,
12681268
(zoom) => {
1269+
const zoomFactor = (zoom ?? 100) / 100;
1270+
12691271
if (proxy.$superdoc.config.useLayoutEngine !== false) {
1270-
PresentationEditor.setGlobalZoom((zoom ?? 100) / 100);
1272+
PresentationEditor.setGlobalZoom(zoomFactor);
1273+
} else {
1274+
// Web layout without layout engine — apply CSS transform directly
1275+
// to non-PDF sub-document containers so zoom works for PM fallback rendering.
1276+
// PDF documents are excluded because pdfViewer.updateScale() handles their zoom
1277+
// separately below; applying both would result in double-zoom.
1278+
const subDocs = layers.value?.querySelectorAll('.superdoc__sub-document');
1279+
subDocs?.forEach((el) => {
1280+
if (el.querySelector('.sd-pdf-viewer')) return;
1281+
if (zoomFactor === 1) {
1282+
el.style.transformOrigin = '';
1283+
el.style.transform = '';
1284+
el.style.width = '';
1285+
} else {
1286+
el.style.transformOrigin = 'top left';
1287+
el.style.transform = `scale(${zoomFactor})`;
1288+
el.style.width = `${100 / zoomFactor}%`;
1289+
}
1290+
});
12711291
}
12721292
12731293
const pdfViewer = getPDFViewer();
1274-
pdfViewer?.updateScale((zoom ?? 100) / 100);
1294+
pdfViewer?.updateScale(zoomFactor);
12751295
12761296
nextTick(() => {
12771297
updateWhiteboardPageSizes();

0 commit comments

Comments
 (0)