Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions packages/layout-engine/painters/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const createDomPainter = (
getActiveComment?: () => string | null;
getPaintSnapshot?: () => PaintSnapshot | null;
onScroll?: () => void;
setZoom?: (zoom: number) => void;
} => {
const painter = new DomPainter(options.blocks, options.measures, {
pageStyles: options.pageStyles,
Expand Down Expand Up @@ -167,5 +168,9 @@ export const createDomPainter = (
onScroll() {
painter.onScroll();
},
// Notify painter of CSS transform scale so virtualization maps scroll correctly
setZoom(zoom: number) {
painter.setZoom(zoom);
},
};
};
33 changes: 30 additions & 3 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,8 @@ export class DomPainter {
private onScrollHandler: ((e: Event) => void) | null = null;
private onWindowScrollHandler: ((e: Event) => void) | null = null;
private onResizeHandler: ((e: Event) => void) | null = null;
/** CSS zoom/scale factor applied to the mount element via transform: scale(). Defaults to 1 (no zoom). */
private zoomFactor = 1;
private sdtHover = new SdtGroupedHover();
/** The currently active/selected comment ID for highlighting */
private activeCommentId: string | null = null;
Expand Down Expand Up @@ -1081,6 +1083,26 @@ export class DomPainter {
}
}

/**
* Sets the CSS zoom/scale factor applied to the mount element.
*
* When the mount element has `transform: scale(zoom)`, getBoundingClientRect()
* returns screen-space coordinates (scaled), but internal layout offsets are in
* unscaled layout space. This factor is used to convert between the two spaces
* during virtualization window calculations.
*
* @param zoom - The zoom/scale factor (e.g., 0.75 for 75% zoom). Defaults to 1.
*/
public setZoom(zoom: number): void {
const next = typeof zoom === 'number' && Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
if (next !== this.zoomFactor) {
this.zoomFactor = next;
if (this.virtualEnabled && this.mount) {
this.updateVirtualWindow();
}
}
}

/**
* Sets the active comment ID for highlighting.
* When set, only the active comment's range is highlighted.
Expand Down Expand Up @@ -1610,16 +1632,21 @@ export class DomPainter {
return;
}

// Map scrollTop -> anchor page index via prefix sums
// Map scrollTop -> anchor page index via prefix sums.
// virtualOffsets are in layout (unscaled) space, so scrollY must also be in layout space.
// When the mount has transform: scale(zoom), getBoundingClientRect() returns
// screen-space values that must be divided by zoom to get layout-space coordinates.
const paddingTop = this.getMountPaddingTopPx();
const zoom = this.zoomFactor;
let scrollY: number;
const isContainerScrollable = this.mount.scrollHeight > this.mount.clientHeight + 1;
if (isContainerScrollable) {
scrollY = Math.max(0, this.mount.scrollTop - paddingTop);
} else {
const rect = this.mount.getBoundingClientRect();
// Translate viewport scroll to content-space scroll offset
scrollY = Math.max(0, -rect.top - paddingTop);
// rect.top is in screen space (affected by CSS transform: scale).
// Divide by zoom to convert to layout space for comparison with virtualOffsets.
scrollY = Math.max(0, -rect.top / zoom - paddingTop);
}

// Binary search for anchor index such that topOfIndex(i) <= scrollY < topOfIndex(i+1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,10 +610,12 @@ export class PresentationEditor extends EventEmitter {
this.#setupInputBridge();
this.#syncTrackedChangesPreferences();

// Register this instance in the static registry
if (options.documentId) {
PresentationEditor.#instances.set(options.documentId, this);
}
// Register this instance in the static registry.
// Generate a fallback ID when documentId is not provided (e.g., blank documents)
// so that setGlobalZoom() can always find and update all instances.
const registryKey = options.documentId || `__anonymous_${Date.now()}_${Math.random().toString(36).slice(2)}`;
this.#options.documentId = registryKey;
Comment thread
caio-pizzol marked this conversation as resolved.
Outdated
PresentationEditor.#instances.set(registryKey, this);

this.#pendingDocChange = true;
this.#scheduleRerender();
Expand Down Expand Up @@ -2213,6 +2215,8 @@ export class PresentationEditor extends EventEmitter {
}
this.#layoutOptions.zoom = zoom;
this.#applyZoom();
// Notify DomPainter so virtualization accounts for the CSS transform scale
this.#domPainter?.setZoom?.(zoom);
this.emit('zoomChange', { zoom });
this.#scheduleSelectionUpdate();
// Trigger cursor updates on zoom changes
Expand Down Expand Up @@ -3377,17 +3381,30 @@ export class PresentationEditor extends EventEmitter {

#ensurePainter(blocks: FlowBlock[], measures: Measure[]) {
if (!this.#domPainter) {
// Ensure the virtualization gap matches the effective page gap so that
// DomPainter's spacer/offset math stays consistent with #applyZoom() height calculations.
const virtualization = this.#layoutOptions.virtualization;
const effectiveGap = this.#getEffectivePageGap();
const normalizedVirtualization = virtualization?.enabled
? { ...virtualization, gap: virtualization.gap ?? effectiveGap }
: virtualization;

this.#domPainter = createDomPainter({
blocks,
measures,
layoutMode: this.#layoutOptions.layoutMode ?? 'vertical',
virtualization: this.#layoutOptions.virtualization,
virtualization: normalizedVirtualization,
pageStyles: this.#layoutOptions.pageStyles,
headerProvider: this.#headerFooterSession?.headerDecorationProvider,
footerProvider: this.#headerFooterSession?.footerDecorationProvider,
ruler: this.#layoutOptions.ruler,
pageGap: this.#layoutState.layout?.pageGap ?? this.#getEffectivePageGap(),
pageGap: this.#layoutState.layout?.pageGap ?? effectiveGap,
});
// Pass the current zoom so virtualization accounts for the CSS transform scale
const currentZoom = this.#layoutOptions.zoom ?? 1;
if (currentZoom !== 1) {
this.#domPainter.setZoom(currentZoom);
}
}
return this.#domPainter;
}
Expand Down Expand Up @@ -4849,7 +4866,9 @@ export class PresentationEditor extends EventEmitter {

this.#viewportHost.style.width = `${scaledWidth}px`;
this.#viewportHost.style.minWidth = `${scaledWidth}px`;
this.#viewportHost.style.minHeight = `${scaledHeight}px`;
this.#viewportHost.style.minHeight = '';
this.#viewportHost.style.height = `${scaledHeight}px`;
Comment thread
caio-pizzol marked this conversation as resolved.
Outdated
this.#viewportHost.style.overflow = 'hidden';
this.#viewportHost.style.transform = '';

this.#painterHost.style.width = `${totalWidth}px`;
Expand All @@ -4872,13 +4891,24 @@ export class PresentationEditor extends EventEmitter {
//
// This ensures the scroll container sees the correct scaled content size while
// the transform provides visual scaling.
//
// IMPORTANT: CSS transform: scale() does NOT change the element's CSS box dimensions.
// At zoom < 1, painterHost's CSS box stays at the full unscaled height while its
// visual size is smaller. Without overflow: hidden, the CSS box would push viewportHost
// taller than intended, creating extra scrollable space at the bottom.
// Using explicit height + overflow: hidden ensures the scroll container sees only
// the scaled visual size.
const scaledWidth = maxWidth * zoom;
const scaledHeight = totalHeight * zoom;

// Set viewport to scaled dimensions for scroll container
// Set viewport to scaled dimensions for scroll container.
// Use explicit height (not just minHeight) with overflow: hidden to prevent
// painterHost's unscaled CSS box from inflating the scroll range.
this.#viewportHost.style.width = `${scaledWidth}px`;
this.#viewportHost.style.minWidth = `${scaledWidth}px`;
this.#viewportHost.style.minHeight = `${scaledHeight}px`;
this.#viewportHost.style.minHeight = '';
this.#viewportHost.style.height = `${scaledHeight}px`;
this.#viewportHost.style.overflow = 'hidden';
Comment thread
tupizz marked this conversation as resolved.
Outdated
this.#viewportHost.style.transform = '';

// Set painterHost to UNSCALED dimensions and apply transform
Expand Down
2 changes: 2 additions & 0 deletions packages/superdoc/src/stores/superdoc-store.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { ref, reactive, computed } from 'vue';
import { v4 as uuidv4 } from 'uuid';
import { useCommentsStore } from './comments-store';
import { getFileObject, DOCX, PDF } from '@superdoc/common';
import { normalizeDocumentEntry } from '@superdoc/core/helpers/file.js';
Expand Down Expand Up @@ -78,6 +79,7 @@ export const useSuperdocStore = defineStore('superdoc', () => {
if (!configDocs?.length && !config.modules.collaboration) {
const newDoc = await getFileObject(BlankDOCX, 'blank.docx', DOCX);
const newDocConfig = {
id: uuidv4(),
type: DOCX,
data: newDoc,
name: 'blank.docx',
Expand Down
Loading