diff --git a/projects/copyleaks-web-report/src/lib/components/containers/content-viewer-container/content-viewer-container.component.ts b/projects/copyleaks-web-report/src/lib/components/containers/content-viewer-container/content-viewer-container.component.ts index 3a5ba657..7ca4a228 100644 --- a/projects/copyleaks-web-report/src/lib/components/containers/content-viewer-container/content-viewer-container.component.ts +++ b/projects/copyleaks-web-report/src/lib/components/containers/content-viewer-container/content-viewer-container.component.ts @@ -15,8 +15,8 @@ import { TemplateRef, ViewChild, } from '@angular/core'; -import { PostMessageEvent, ZoomEvent } from '../../../models/report-iframe-events.models'; -import { IReportViewEvent } from '../../../models/report-view.models'; +import { PostMessageEvent, ScrollPositionEvent, ZoomEvent } from '../../../models/report-iframe-events.models'; +import { EReportViewTab, IReportViewEvent, IScrollPositionState } from '../../../models/report-view.models'; import { Match, MatchType, @@ -79,6 +79,26 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O if (source !== this.iFrameWindow) { return; } + + if (data.type === 'iframeReady') { + if (this._iframeReadyResolver) { + clearTimeout(this._iframeReadyTimeout); + this._iframeReadyResolver(); + this._iframeReadyResolver = null; + } + return; + } + + // Handle scroll position updates from iframe + if (data.type === 'scrollPosition') { + if (this._ignoreScrollUpdates || this._isRestoringScroll) { + return; + } + + const scrollEvent = data as ScrollPositionEvent; + this.viewSvc.setIframeScrollPosition(scrollEvent.scrollTop, scrollEvent.scrollLeft); + return; + } this.iFrameMessageEvent.emit(data as PostMessageEvent); } @@ -438,6 +458,12 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O private _zoomIn: boolean; + private _ignoreScrollUpdates = false; + private _isRestoringScroll = false; + private _pendingScrollRestore: { scrollTop: number; scrollLeft: number; page?: number } | null = null; + private _iframeReadyTimeout: any; + private _iframeReadyResolver: ((value: void) => void) | null = null; + constructor( private _renderer: Renderer2, private _cdr: ChangeDetectorRef, @@ -449,6 +475,17 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O ngOnInit(): void { if (this.flexGrow !== undefined && this.flexGrow !== null) this.flexGrowProp = this.flexGrow; + const currentTab = this._getCurrentTab(); + const customTabId = this.viewSvc.reportViewMode$.value.selectedCustomTabId; + this.viewSvc.setCurrentViewTab(currentTab); + // Handle initial load for custom tab in text view + if (!this.isHtmlView && currentTab === EReportViewTab.Custom) { + this._prepareForReload(); + this._queueScrollRestore(currentTab, customTabId); + setTimeout(() => { + this._restoreScrollPosition(); + }, 100); + } if (!this.isExportedComponent) { this.viewSvc.selectedCustomTabContent$.pipe(untilDestroy(this)).subscribe(content => { if (this.viewMode !== 'one-to-one') this.customTabContent = content; @@ -483,6 +520,14 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O this.iFrameWindow = this.contentIFrame?.nativeElement?.contentWindow; this.iframeLoaded = true; this.showLoadingView = false; + const currentTab = this._getCurrentTab(); + const customTabId = this.viewSvc.reportViewMode$.value.selectedCustomTabId; + // Handle iframe scroll restoration + this._waitForIframeReady().then(() => { + this._prepareForReload(); + this._queueScrollRestore(currentTab, customTabId); + this._restoreScrollPosition(); + }); } }, false @@ -542,6 +587,103 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O } ngOnChanges(changes: SimpleChanges): void { + // Detect tab changes and save/restore scroll position + let isHandlingTabSwitch = false; + if ( + 'isAIView' in changes || + 'viewMode' in changes || + 'contentHtmlMatches' in changes || + 'contentTextMatches' in changes + ) { + const previousTab = this.viewSvc.currentViewTab; + const newTab = this._getCurrentTab(); + const previousCustomTabId = this.viewSvc.previousCustomTabId; + const currentCustomTabId = this.viewSvc.currentCustomTabId; + + const isSwitchingBetweenSharedTabs = + previousTab !== EReportViewTab.Custom && newTab !== EReportViewTab.Custom && previousTab !== newTab; + + const isSwitchingToCustomTab = previousTab !== EReportViewTab.Custom && newTab === EReportViewTab.Custom; + + const isSwitchingFromCustomTab = previousTab === EReportViewTab.Custom && newTab !== EReportViewTab.Custom; + + if (newTab === EReportViewTab.Custom && currentCustomTabId === 'ai-overview') { + this.viewSvc.clearScrollPositions(); + } + + // Handle save/restore for shared instance tabs switching between each other + if (isSwitchingBetweenSharedTabs) { + isHandlingTabSwitch = true; + let previousIsHtmlView = this.isHtmlView; + if ('isHtmlView' in changes && !changes['isHtmlView'].firstChange) { + previousIsHtmlView = changes['isHtmlView'].previousValue; + } + let previousPage = this.currentPage; + if ('currentPage' in changes && !changes['currentPage'].firstChange) { + previousPage = changes['currentPage'].previousValue; + } + + this._saveScrollPosition(previousTab, undefined, previousIsHtmlView, previousPage); + + this._prepareForReload(); + this._queueScrollRestore(newTab); + if (!this.isHtmlView) { + // For text view, restore after DOM updates + setTimeout(() => { + this._restoreScrollPosition(); + }, 100); + } + this.viewSvc.setCurrentViewTab(newTab); + } else if (isSwitchingToCustomTab && currentCustomTabId !== 'ai-overview') { + isHandlingTabSwitch = true; + let previousIsHtmlView = this.viewSvc.currentIsHtmlView; + let previousPage = this.currentPage; + if ('currentPage' in changes && !changes['currentPage'].firstChange) { + previousPage = changes['currentPage'].previousValue; + } + this._saveScrollPosition(previousTab, undefined, previousIsHtmlView, previousPage); + this.viewSvc.setCurrentViewTab(newTab); + } else if (isSwitchingFromCustomTab && previousCustomTabId !== 'ai-overview') { + isHandlingTabSwitch = true; + this._prepareForReload(); + this._queueScrollRestore(newTab, currentCustomTabId); + // For text view, restore after DOM updates + if (!this.isHtmlView) { + setTimeout(() => { + this._restoreScrollPosition(); + }, 100); + } + this.viewSvc.setCurrentViewTab(newTab); + } else { + this.viewSvc.setCurrentViewTab(newTab); + } + } + + // Handle page changes (text view only) + if ('currentPage' in changes && !changes['currentPage']?.firstChange && !isHandlingTabSwitch) { + if (this._pendingScrollRestore && this._isRestoringScroll) { + const savedPage = this._pendingScrollRestore.page; + if (savedPage && savedPage === this.currentPage) { + this._restoreScrollPosition(); + } + } + } + + if ('isHtmlView' in changes && !changes['isHtmlView'].firstChange) { + const currentTab = this._getCurrentTab(); + const customTabId = this.viewSvc.reportViewMode$.value.selectedCustomTabId; + + this._prepareForReload(); + this._queueScrollRestore(currentTab, customTabId); + + // For text view, restore immediately after DOM updates + if (!this.isHtmlView) { + setTimeout(() => { + this._restoreScrollPosition(); + }, 100); + } + } + if ( (('contentTextMatches' in changes && changes['contentTextMatches'].currentValue != undefined) || ('customViewMatchesData' in changes && changes['customViewMatchesData'].currentValue != undefined)) && @@ -646,6 +788,10 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O } onViewChange() { + const currentTab = this._getCurrentTab(); + const customTabId = this.viewSvc.reportViewMode$.value.selectedCustomTabId; + this._saveScrollPosition(currentTab, customTabId, this.isHtmlView, this.currentPage); + if (!this.iframeLoaded) this.showLoadingView = true; if (this.viewSvc.reportViewMode.alertCode === ALERTS.SUSPECTED_AI_TEXT_DETECTED) { this._highlightService.aiInsightsSelectedResults?.forEach(result => { @@ -741,6 +887,194 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O }); } + private _getCurrentTab(): EReportViewTab { + const selectedCustomTabId = this.viewSvc.reportViewMode$.value.selectedCustomTabId; + if (selectedCustomTabId) return EReportViewTab.Custom; + return this.isAIView + ? EReportViewTab.AIContent + : this.viewMode == 'writing-feedback' + ? EReportViewTab.WritingAssistant + : EReportViewTab.MatchedText; + } + + /** + * Prepare for content reload (view change, page change, tab change) + */ + private _prepareForReload(): void { + this._ignoreScrollUpdates = true; + this._isRestoringScroll = true; + } + + /** + * Queue scroll restoration + */ + private _queueScrollRestore(tab: EReportViewTab, customTabId?: string): void { + const savedState = this.viewSvc.getScrollPosition(tab, this.isHtmlView, customTabId); + + if (savedState) { + this._pendingScrollRestore = { + scrollTop: savedState.scrollTop, + scrollLeft: savedState.scrollLeft, + }; + + // Store the saved page for text view restoration + if (!this.isHtmlView) { + this._pendingScrollRestore.page = savedState.page; + } + + // For wrapper scroll (text view or custom tabs), restore immediately + if (this._isWrapperScroll()) { + setTimeout(() => { + this._restoreScrollPosition(); + }, 0); + } + // For iframe view, restoration will happen when iframe is ready + } else { + this._clearRestorationFlags(); + } + } + + /** + * Save current scroll position + */ + private _saveScrollPosition(tab: EReportViewTab, customTabId?: string, isHtmlView?: boolean, page?: number): void { + let scrollTop = 0; + let scrollLeft = 0; + + const currentIsHtmlView = isHtmlView !== undefined ? isHtmlView : this.isHtmlView; + + if (this._isWrapperScroll(currentIsHtmlView)) { + // Wrapper container scroll (text view) + let wrapperContainer = document + .querySelector('.content-container') + ?.querySelector('.content-container') as HTMLElement; + if (!wrapperContainer) { + wrapperContainer = document.querySelector('.content-container') as HTMLElement; + } + if (wrapperContainer) { + scrollTop = wrapperContainer.scrollTop; + scrollLeft = wrapperContainer.scrollLeft; + } + } else { + // HTML/PDF iframe view - use tracked position from service + const iframeScroll = this.viewSvc.iframeScrollPosition; + scrollTop = iframeScroll.top; + scrollLeft = iframeScroll.left; + } + + const state: IScrollPositionState = { + tab: tab, + origin: this.reportOrigin, + page: page !== undefined ? page : this.currentPage, + isHtmlView: currentIsHtmlView, + scrollTop, + scrollLeft, + customTabId: tab === EReportViewTab.Custom ? customTabId : undefined, + }; + + this.viewSvc.saveScrollPosition(state); + } + + /** + * Restore scroll position from pending data + */ + private _restoreScrollPosition(): void { + if (!this._pendingScrollRestore) { + this._clearRestorationFlags(); + return; + } + + const { scrollTop, scrollLeft, page } = this._pendingScrollRestore; + + if (this._isWrapperScroll()) { + if (page && page !== this.currentPage && !this.isHtmlView) { + // Emit event to change to the saved page + if (this.reportOrigin === 'original' || this.reportOrigin === 'source') { + this.viewChangeEvent.emit({ + ...this.viewSvc.reportViewMode, + sourcePageIndex: page, + }); + } else { + this.viewChangeEvent.emit({ + ...this.viewSvc.reportViewMode, + suspectPageIndex: page, + }); + } + return; + } + + // Wrapper container scroll + let wrapperContainer = document + .querySelector('.content-container') + ?.querySelector('.content-container') as HTMLElement; + + if (!wrapperContainer) { + wrapperContainer = document.querySelector('.content-container') as HTMLElement; + } + + if (wrapperContainer) { + wrapperContainer.scrollTop = scrollTop; + wrapperContainer.scrollLeft = scrollLeft; + } + this._pendingScrollRestore = null; + this._clearRestorationFlags(); + } else if (this.isHtmlView && this.iframeLoaded) { + const iframe = this.contentIFrame?.nativeElement; + if (iframe?.contentWindow) { + this.viewSvc.setIframeScrollPosition(scrollTop, scrollLeft); + + iframe.contentWindow.postMessage( + { + type: 'setScroll', + scrollTop: scrollTop, + scrollLeft: scrollLeft, + }, + '*' + ); + + setTimeout(() => { + this._pendingScrollRestore = null; + this._clearRestorationFlags(); + }, 100); + } else { + this._clearRestorationFlags(); + } + } + } + + /** + * Clear restoration flags + */ + private _clearRestorationFlags(): void { + this._ignoreScrollUpdates = false; + this._isRestoringScroll = false; + } + + /** + * Check if scroll should be on wrapper container (not iframe) + */ + private _isWrapperScroll(isHtmlView?: boolean): boolean { + const htmlView = isHtmlView !== undefined ? isHtmlView : this.isHtmlView; + + if (htmlView && this.hasHtml) { + return false; + } + return true; + } + + private _waitForIframeReady(): Promise { + return new Promise(resolve => { + // Store the resolver so we can call it when we receive the message + this._iframeReadyResolver = resolve; + + // Set a timeout in case the message never arrives (fallback) + this._iframeReadyTimeout = setTimeout(() => { + this._iframeReadyResolver = null; + resolve(); + }, 2000); // 2 second timeout + }); + } + /** * Jump to next match click handler. * @param next if `true` jump to next match, otherwise jumpt to previous match @@ -1138,6 +1472,17 @@ export class ContentViewerContainerComponent implements OnInit, AfterViewInit, O } ngOnDestroy(): void { + if (this._iframeReadyTimeout) { + clearTimeout(this._iframeReadyTimeout); + } + const currentTab = this.viewSvc.currentViewTab; + const customTabId = this.viewSvc.previousCustomTabId; + + // Save scroll position on destroy for custom tabs (except AI Overview) + if (currentTab === EReportViewTab.Custom && customTabId && customTabId !== 'ai-overview') { + this._saveScrollPosition(currentTab, customTabId); + } + this.docDirObserver?.disconnect(); this.contentTextChangesObserver?.disconnect(); } diff --git a/projects/copyleaks-web-report/src/lib/models/report-iframe-events.models.ts b/projects/copyleaks-web-report/src/lib/models/report-iframe-events.models.ts index cbddba84..6e9ccc38 100644 --- a/projects/copyleaks-web-report/src/lib/models/report-iframe-events.models.ts +++ b/projects/copyleaks-web-report/src/lib/models/report-iframe-events.models.ts @@ -9,7 +9,11 @@ export type PostMessageEvent = | MultiMatchSelectEvent | CorrectionSelectEvent | MatchesRefreshEvent - | MatchGroupSelectEvent; + | MatchGroupSelectEvent + | ScrollPositionEvent + | SetScrollEvent + | GetScrollEvent + | IframeReadyEvent; /** base type of post message event */ interface BasePostMessageEvent { @@ -77,3 +81,37 @@ export interface CorrectionSelectEvent extends BasePostMessageEvent { /** the index of the match that was selected */ gid: number; } + +/** Event type indicating scroll position update from iframe */ +export interface ScrollPositionEvent extends BasePostMessageEvent { + type: 'scrollPosition'; + /** the scroll top position */ + scrollTop: number; + /** the scroll left position */ + scrollLeft: number; +} + +/** Event type indicating a request to set scroll position in iframe */ +export interface SetScrollEvent extends BasePostMessageEvent { + type: 'setScroll'; + /** the scroll top position to set */ + scrollTop: number; + /** the scroll left position to set */ + scrollLeft: number; +} + +/** Event type indicating a request to get current scroll position from iframe */ +export interface GetScrollEvent extends BasePostMessageEvent { + type: 'getScroll'; +} + +/** Event type indicating iframe content is fully loaded and ready */ +export interface IframeReadyEvent extends BasePostMessageEvent { + type: 'iframeReady'; + /** the scroll height of the iframe content */ + scrollHeight: number; + /** current scroll top position */ + scrollTop: number; + /** current scroll left position */ + scrollLeft: number; +} diff --git a/projects/copyleaks-web-report/src/lib/models/report-view.models.ts b/projects/copyleaks-web-report/src/lib/models/report-view.models.ts index 4e885d7c..50359afa 100644 --- a/projects/copyleaks-web-report/src/lib/models/report-view.models.ts +++ b/projects/copyleaks-web-report/src/lib/models/report-view.models.ts @@ -29,6 +29,13 @@ export interface IReportViewQueryParams { showAIPhrases?: string; } +export enum EReportViewTab { + MatchedText = 'matched-text', + AIContent = 'ai-content', + WritingAssistant = 'writing-assistant', + Custom = 'custom', +} + export interface IReportResponsiveMode { mode: EResponsiveLayoutType; } @@ -74,3 +81,37 @@ export interface IPercentageModel { /** Indicates whether this percentage is enabled or disabled. */ disabled: boolean; } + +/** + * Represents the scroll position state for a specific view configuration. + */ +export interface IScrollPositionState { + /** The tab type */ + tab: EReportViewTab; + + /** The report origin (source/original/suspect) */ + origin: 'source' | 'original' | 'suspect'; + + /** The current page number */ + page: number; + + /** Whether HTML view is active */ + isHtmlView: boolean; + + /** Vertical scroll position */ + scrollTop: number; + + /** Horizontal scroll position */ + scrollLeft: number; + + /** The custom tab ID (for Custom tab type only) */ + customTabId?: string; +} + +/** + * Map of scroll positions keyed by unique view identifier. + * Key format: "tab_origin_page_isHtmlView" + */ +export interface IScrollPositionStateMap { + [key: string]: IScrollPositionState; +} diff --git a/projects/copyleaks-web-report/src/lib/services/report-view.service.ts b/projects/copyleaks-web-report/src/lib/services/report-view.service.ts index 916008c8..c1e84b5f 100644 --- a/projects/copyleaks-web-report/src/lib/services/report-view.service.ts +++ b/projects/copyleaks-web-report/src/lib/services/report-view.service.ts @@ -1,7 +1,14 @@ import { Injectable, TemplateRef } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { ResultDetailItem } from '../models/report-matches.models'; -import { IReportResponsiveMode, IReportViewEvent } from '../models/report-view.models'; +import { + EReportViewTab, + IReportResponsiveMode, + IReportViewEvent, + IScrollPositionState, + IScrollPositionStateMap, +} from '../models/report-view.models'; +import { distinctUntilChanged, map } from 'rxjs/operators'; @Injectable() export class ReportViewService { @@ -84,10 +91,121 @@ export class ReportViewService { public get documentDirection() { return this._documentDirection$.value; } + private _scrollPositionStates$ = new BehaviorSubject({}); + /** Subject for maintaining scroll position states across different views. */ + public get scrollPositionStates$() { + return this._scrollPositionStates$; + } + /** Getter for scroll position states. */ + public get scrollPositionStates() { + return this._scrollPositionStates$.value; + } + + private _currentViewTab$ = new BehaviorSubject(EReportViewTab.MatchedText); + + private _previousCustomTabId: string | undefined = undefined; + private _currentCustomTabId: string | undefined = undefined; + + private _iframeScrollPosition$ = new BehaviorSubject<{ top: number; left: number }>({ top: 0, left: 0 }); + + private _previousIsHtmlView: boolean = false; + private _currentIsHtmlView: boolean = false; + private observer: MutationObserver; constructor() { this._observeDocumentDirection(); + // Track custom tab ID changes + this._reportViewMode$ + .pipe( + map(mode => mode.selectedCustomTabId), + distinctUntilChanged() + ) + .subscribe(newCustomTabId => { + if (this._currentCustomTabId !== newCustomTabId) { + this._previousCustomTabId = this._currentCustomTabId; + this._currentCustomTabId = newCustomTabId; + } + }); + + this._reportViewMode$ + .pipe( + map(mode => mode.isHtmlView), + distinctUntilChanged() + ) + .subscribe(newIsHtmlView => { + if (this._currentIsHtmlView !== newIsHtmlView) { + this._previousIsHtmlView = this._currentIsHtmlView; + this._currentIsHtmlView = newIsHtmlView; + } + }); + } + + public get currentCustomTabId(): string | undefined { + return this._currentCustomTabId; + } + + public get previousCustomTabId(): string | undefined { + return this._previousCustomTabId; + } + + /** + * Get the previous isHtmlView state (before the last change) + */ + public get previousIsHtmlView(): boolean { + return this._previousIsHtmlView; + } + + /** + * Get the current isHtmlView state + */ + public get currentIsHtmlView(): boolean { + return this._currentIsHtmlView; + } + + /** + * Save scroll position for a specific tab. + * @param state The scroll position state to save + */ + public saveScrollPosition(state: IScrollPositionState): void { + const key = this._getScrollStateKey(state.tab, state.isHtmlView, state.customTabId); + const currentStates = { ...this.scrollPositionStates }; + currentStates[key] = state; + this._scrollPositionStates$.next(currentStates); + } + + /** + * Get scroll position for a specific tab. + * @param tab The tab type + * @param isHtmlView Whether HTML view is active + * @param customTabId The custom tab ID (for Custom tab type only) + * @returns The saved scroll position state, or null if not found + */ + public getScrollPosition( + tab: EReportViewTab, + isHtmlView: boolean, + customTabId?: string + ): IScrollPositionState | null { + const key = this._getScrollStateKey(tab, isHtmlView, customTabId); + return this.scrollPositionStates[key] || null; + } + + /** + * Clear all saved scroll positions. + */ + public clearScrollPositions(): void { + this._scrollPositionStates$.next({}); + } + + /** + * Generate a unique key for scroll position state. + * @private + */ + private _getScrollStateKey(tab: EReportViewTab, isHtmlView: boolean, customTabId?: string): string { + if (tab === EReportViewTab.Custom && customTabId) { + return `${tab}_${isHtmlView}_${customTabId}`; + } + return `${tab}_${isHtmlView}`; } /** @@ -103,6 +221,54 @@ export class ReportViewService { }); } + /** + * Observable for the current active tab + */ + public get currentViewTab$() { + return this._currentViewTab$.asObservable(); + } + + /** + * Get the current active tab + */ + public get currentViewTab() { + return this._currentViewTab$.value; + } + + /** + * Set the current active tab + */ + public setCurrentViewTab(tab: EReportViewTab) { + this._currentViewTab$.next(tab); + } + + /** + * Observable for iframe scroll position + */ + public get iframeScrollPosition$() { + return this._iframeScrollPosition$.asObservable(); + } + + /** + * Get the current iframe scroll position + */ + public get iframeScrollPosition() { + return this._iframeScrollPosition$.value; + } + + /** + * Update the iframe scroll position + */ + public setIframeScrollPosition(top: number, left: number) { + this._iframeScrollPosition$.next({ top, left }); + } + + /** + * Clear the iframe scroll position + */ + public clearIframeScrollPosition() { + this._iframeScrollPosition$.next({ top: 0, left: 0 }); + } /** * Get the document direction (ltr/rtl). * @returns The document direction (ltr/rtl). diff --git a/projects/copyleaks-web-report/src/lib/utils/one-to-many-iframe-logic.ts b/projects/copyleaks-web-report/src/lib/utils/one-to-many-iframe-logic.ts index 5751e8fe..0df930f1 100644 --- a/projects/copyleaks-web-report/src/lib/utils/one-to-many-iframe-logic.ts +++ b/projects/copyleaks-web-report/src/lib/utils/one-to-many-iframe-logic.ts @@ -83,6 +83,25 @@ function ready() { elem.addEventListener('mouseenter', onMatchHover); elem.addEventListener('mouseleave', onMatchHover); }); + + // Setup scroll tracking with debounce + let scrollTimeout: any; + const handleScroll = () => { + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + sendScrollPosition(); + }, 300); + }; + + // Attach scroll listener to the appropriate element + if (isPdf) { + const pageContainer = document.getElementById('page-container'); + if (pageContainer) { + pageContainer.addEventListener('scroll', handleScroll); + } + } else { + window.addEventListener('scroll', handleScroll); + } } /** @@ -127,6 +146,12 @@ function ready() { case 'correction-select': handelCorrectionSelect(event); break; + case 'setScroll': + handleSetScroll(event); + break; + case 'getScroll': + handleGetScroll(); + break; default: } } @@ -368,6 +393,74 @@ function ready() { elem?.classList?.toggle('hover'); } } + /** + * Get the current scroll position from the appropriate element + */ + function getScrollPosition() { + let scrollTop = 0; + let scrollLeft = 0; + + if (isPdf) { + const pageContainer = document.getElementById('page-container'); + if (pageContainer) { + scrollTop = pageContainer.scrollTop; + scrollLeft = pageContainer.scrollLeft; + } + } else { + scrollTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop; + scrollLeft = window.scrollX || document.documentElement.scrollLeft || document.body.scrollLeft; + } + + return { scrollTop, scrollLeft }; + } + + /** + * Set scroll position on the appropriate element + */ + function setScrollPosition(scrollTop: number, scrollLeft: number) { + if (isPdf) { + const pageContainer = document.getElementById('page-container'); + if (pageContainer) { + pageContainer.scrollTop = scrollTop; + pageContainer.scrollLeft = scrollLeft; + } + } else { + window.scrollTo(scrollLeft, scrollTop); + // Fallback for older browsers + document.documentElement.scrollTop = scrollTop; + document.body.scrollTop = scrollTop; + document.documentElement.scrollLeft = scrollLeft; + document.body.scrollLeft = scrollLeft; + } + } + + /** + * Handle setScroll message from parent + */ + function handleSetScroll(event: any) { + const { scrollTop, scrollLeft } = event; + setScrollPosition(scrollTop, scrollLeft); + } + + /** + * Handle getScroll message from parent + */ + function handleGetScroll() { + sendScrollPosition(); + } + + /** + * Send current scroll position to parent + */ + function sendScrollPosition() { + const { scrollTop, scrollLeft } = getScrollPosition(); + + messageParent({ + type: 'scrollPosition', + scrollTop: scrollTop, + scrollLeft: scrollLeft, + }); + } function zoomIn() { currentZoom += 0.1; @@ -539,6 +632,29 @@ function ready() { return false; } + + // Add this function at the end of the ready() function, just before the closing brace + function notifyParentReady() { + // Wait for all resources to load + window.addEventListener('load', function () { + // Wait for next animation frame to ensure rendering is complete + requestAnimationFrame(function () { + requestAnimationFrame(function () { + // Send ready message to parent + const { scrollTop, scrollLeft } = getScrollPosition(); + messageParent({ + type: 'iframeReady', + scrollHeight: document.body.scrollHeight, + scrollTop: scrollTop, + scrollLeft: scrollLeft, + }); + }); + }); + }); + } + + // Call it at the end of ready() function + notifyParentReady(); } export default `(${onDocumentReady.toString()})(${ready.toString()})`;