|
|
|
|
@@ -105,6 +105,9 @@ export class DeDocumentViewer extends DeesElement {
|
|
|
|
|
@state()
|
|
|
|
|
accessor thumbnailPages: number[] = [];
|
|
|
|
|
|
|
|
|
|
// Zoom animation
|
|
|
|
|
private zoomAnimationId: number | null = null;
|
|
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
|
|
|
|
@@ -905,31 +908,69 @@ export class DeDocumentViewer extends DeesElement {
|
|
|
|
|
this.updateDisplayZoom();
|
|
|
|
|
} else if (value === "fit-width") {
|
|
|
|
|
// Calculate zoom to fit page width to viewport width
|
|
|
|
|
this.calculateFitWidth();
|
|
|
|
|
this.calculateFitWidth(true);
|
|
|
|
|
} else if (value === "fit-page") {
|
|
|
|
|
// Calculate zoom to fit entire page in viewport
|
|
|
|
|
this.calculateFitPage();
|
|
|
|
|
this.calculateFitPage(true);
|
|
|
|
|
} else {
|
|
|
|
|
this.zoomLevel = value;
|
|
|
|
|
this.displayZoom = value;
|
|
|
|
|
// Animate to numeric preset
|
|
|
|
|
this.animateZoomTo(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleZoomStep(delta: number): void {
|
|
|
|
|
const current = this.zoomLevel ?? this.displayZoom;
|
|
|
|
|
const newZoom = Math.min(400, Math.max(25, current + delta));
|
|
|
|
|
this.zoomLevel = newZoom;
|
|
|
|
|
this.displayZoom = newZoom;
|
|
|
|
|
this.animateZoomTo(newZoom);
|
|
|
|
|
this.zoomMode = newZoom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleZoomSlider(e: Event): void {
|
|
|
|
|
// Cancel any running animation when user takes manual control
|
|
|
|
|
if (this.zoomAnimationId !== null) {
|
|
|
|
|
cancelAnimationFrame(this.zoomAnimationId);
|
|
|
|
|
this.zoomAnimationId = null;
|
|
|
|
|
}
|
|
|
|
|
const value = parseInt((e.target as HTMLInputElement).value, 10);
|
|
|
|
|
this.zoomLevel = value;
|
|
|
|
|
this.displayZoom = value;
|
|
|
|
|
this.zoomMode = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Animate zoom level to target value
|
|
|
|
|
* @param targetZoom - Target zoom percentage
|
|
|
|
|
* @param duration - Animation duration in ms (default: 200)
|
|
|
|
|
*/
|
|
|
|
|
private animateZoomTo(targetZoom: number, duration: number = 200): void {
|
|
|
|
|
// Cancel any existing animation
|
|
|
|
|
if (this.zoomAnimationId !== null) {
|
|
|
|
|
cancelAnimationFrame(this.zoomAnimationId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startZoom = this.zoomLevel ?? this.displayZoom;
|
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
|
|
|
|
|
|
|
|
|
|
const animate = (currentTime: number) => {
|
|
|
|
|
const elapsed = currentTime - startTime;
|
|
|
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
|
const easedProgress = easeOutCubic(progress);
|
|
|
|
|
|
|
|
|
|
const currentZoom = startZoom + (targetZoom - startZoom) * easedProgress;
|
|
|
|
|
this.zoomLevel = Math.round(currentZoom);
|
|
|
|
|
this.displayZoom = this.zoomLevel;
|
|
|
|
|
|
|
|
|
|
if (progress < 1) {
|
|
|
|
|
this.zoomAnimationId = requestAnimationFrame(animate);
|
|
|
|
|
} else {
|
|
|
|
|
this.zoomAnimationId = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.zoomAnimationId = requestAnimationFrame(animate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleSpacingPreset(value: number): void {
|
|
|
|
|
this.pageGap = value;
|
|
|
|
|
}
|
|
|
|
|
@@ -1083,19 +1124,24 @@ export class DeDocumentViewer extends DeesElement {
|
|
|
|
|
setTimeout(cleanup, 100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private calculateFitWidth(): void {
|
|
|
|
|
private calculateFitWidth(animate: boolean = false): void {
|
|
|
|
|
const viewport = this.shadowRoot?.querySelector(".viewport");
|
|
|
|
|
if (!viewport) return;
|
|
|
|
|
|
|
|
|
|
// Account for padding and scrollbar width
|
|
|
|
|
const viewportWidth = viewport.clientWidth - 32 - 16;
|
|
|
|
|
const scale = viewportWidth / plugins.shared.A4_WIDTH;
|
|
|
|
|
const targetZoom = Math.round(scale * 100);
|
|
|
|
|
|
|
|
|
|
this.zoomLevel = Math.round(scale * 100);
|
|
|
|
|
this.displayZoom = this.zoomLevel;
|
|
|
|
|
if (animate) {
|
|
|
|
|
this.animateZoomTo(targetZoom);
|
|
|
|
|
} else {
|
|
|
|
|
this.zoomLevel = targetZoom;
|
|
|
|
|
this.displayZoom = targetZoom;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private calculateFitPage(): void {
|
|
|
|
|
private calculateFitPage(animate: boolean = false): void {
|
|
|
|
|
const viewport = this.shadowRoot?.querySelector(".viewport");
|
|
|
|
|
if (!viewport) return;
|
|
|
|
|
|
|
|
|
|
@@ -1110,9 +1156,14 @@ export class DeDocumentViewer extends DeesElement {
|
|
|
|
|
const scaleByHeight = viewportHeight / plugins.shared.A4_HEIGHT;
|
|
|
|
|
const scaleByWidth = viewportWidth / plugins.shared.A4_WIDTH;
|
|
|
|
|
const scale = Math.min(scaleByHeight, scaleByWidth);
|
|
|
|
|
const targetZoom = Math.round(scale * 100);
|
|
|
|
|
|
|
|
|
|
this.zoomLevel = Math.round(scale * 100);
|
|
|
|
|
this.displayZoom = this.zoomLevel;
|
|
|
|
|
if (animate) {
|
|
|
|
|
this.animateZoomTo(targetZoom);
|
|
|
|
|
} else {
|
|
|
|
|
this.zoomLevel = targetZoom;
|
|
|
|
|
this.displayZoom = targetZoom;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateDisplayZoom(): void {
|
|
|
|
|
@@ -1297,14 +1348,42 @@ export class DeDocumentViewer extends DeesElement {
|
|
|
|
|
const pageRect = targetPage.getBoundingClientRect();
|
|
|
|
|
const pageOffsetFromDoc = pageRect.top - docRect.top;
|
|
|
|
|
|
|
|
|
|
// Scroll to the page position
|
|
|
|
|
viewport.scrollTo({
|
|
|
|
|
top: pageOffsetFromDoc,
|
|
|
|
|
behavior: smooth ? "smooth" : "instant",
|
|
|
|
|
});
|
|
|
|
|
// Scroll to the page position with fast custom animation
|
|
|
|
|
if (smooth) {
|
|
|
|
|
this.animateScrollTo(viewport, pageOffsetFromDoc, 250);
|
|
|
|
|
} else {
|
|
|
|
|
viewport.scrollTop = pageOffsetFromDoc;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Animate scroll to target position
|
|
|
|
|
* @param element - The scrollable element
|
|
|
|
|
* @param targetTop - Target scroll position
|
|
|
|
|
* @param duration - Animation duration in ms (default: 150)
|
|
|
|
|
*/
|
|
|
|
|
private animateScrollTo(element: Element, targetTop: number, duration: number = 150): void {
|
|
|
|
|
const startTop = element.scrollTop;
|
|
|
|
|
const distance = targetTop - startTop;
|
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
|
|
|
|
|
|
|
|
|
|
const step = (currentTime: number) => {
|
|
|
|
|
const elapsed = currentTime - startTime;
|
|
|
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
|
const easedProgress = easeOutCubic(progress);
|
|
|
|
|
|
|
|
|
|
element.scrollTop = startTop + distance * easedProgress;
|
|
|
|
|
|
|
|
|
|
if (progress < 1) {
|
|
|
|
|
requestAnimationFrame(step);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(step);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trigger the print dialog
|
|
|
|
|
*/
|
|
|
|
|
|