diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding-iframe.js b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding-iframe.js index 4e21ee22cc..1db934510a 100644 --- a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding-iframe.js +++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding-iframe.js @@ -21,18 +21,28 @@ this.observer = null; this.debounceTimer = null; this.lastReportedHeight = 0; + this.lastReportedUrl = null; // Bind methods to ensure correct 'this' context this.measureHeight = this.measureHeight.bind(this); this.reportHeight = this.reportHeight.bind(this); this.debouncedReportHeight = this.debouncedReportHeight.bind(this); this.onContentChange = this.onContentChange.bind(this); + this.reportUrlChange = this.reportUrlChange.bind(this); + this.handlePopState = this.handlePopState.bind(this); + this.handleHashChange = this.handleHashChange.bind(this); } init() { // Auto-enable for iframe context this.enable(); + // Report initial URL + this.reportUrlChange(); + + // Setup URL change monitoring + this.setupUrlMonitoring(); + // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { @@ -108,6 +118,10 @@ } window.removeEventListener('resize', this.debouncedReportHeight); + + // Remove URL monitoring listeners + window.removeEventListener('popstate', this.handlePopState); + window.removeEventListener('hashchange', this.handleHashChange); } setupMonitoring() { @@ -228,6 +242,83 @@ this.debounceTimer = null; }, this.config.debounceDelay); } + + setupUrlMonitoring() { + // Monitor popstate events (back/forward navigation) + window.addEventListener('popstate', this.handlePopState); + + // Monitor hashchange events + window.addEventListener('hashchange', this.handleHashChange); + + // Override history methods to catch programmatic navigation + this.overrideHistoryMethods(); + + // Monitor for navigation through other means + this.setupNavigationMonitoring(); + } + + overrideHistoryMethods() { + const originalPushState = history.pushState; + const originalReplaceState = history.replaceState; + + history.pushState = (...args) => { + const result = originalPushState.apply(history, args); + setTimeout(() => this.reportUrlChange(), 0); + return result; + }; + + history.replaceState = (...args) => { + const result = originalReplaceState.apply(history, args); + setTimeout(() => this.reportUrlChange(), 0); + return result; + }; + } + + setupNavigationMonitoring() { + // Monitor for clicks on links + document.addEventListener('click', (event) => { + const link = event.target.closest('a'); + if (link && link.href) { + // Delay to allow navigation to complete + setTimeout(() => this.reportUrlChange(), 100); + } + }); + + // Monitor for form submissions + document.addEventListener('submit', () => { + setTimeout(() => this.reportUrlChange(), 100); + }); + } + + handlePopState(event) { + this.reportUrlChange(); + } + + handleHashChange(event) { + this.reportUrlChange(); + } + + reportUrlChange() { + try { + const currentUrl = window.location.href; + + // Only report if URL actually changed + if (currentUrl !== this.lastReportedUrl) { + this.lastReportedUrl = currentUrl; + + // Send URL change to parent + if (window.parent && window.parent !== window) { + window.parent.postMessage({ + type: 'url-change', + url: currentUrl, + timestamp: Date.now() + }, '*'); + } + } + } catch (e) { + console.warn('ABP Embedding Iframe: Failed to report URL change', e); + } + } } // Create and initialize the handler diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css index 9516ad512f..ce7feee4a1 100644 --- a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css +++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css @@ -33,6 +33,7 @@ abp-embedding iframe { -webkit-text-shadow: none !important; -moz-text-shadow: none !important; width: 100%; + min-height: 50vh; /* Minimum height for better UX */ } /* Responsive behavior */ diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js index 70f3ad36c3..7dd6da03b0 100644 --- a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js +++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js @@ -132,13 +132,23 @@ } } + ensureInitialHistoryEntry() { + if (!this.isHistoryManager) return; + + // Only push initial state if there's no fragment currently + if (!window.location.hash || !window.location.hash.startsWith('#page=')) { + // Replace current state to represent the initial iframe state + history.replaceState({ isInitial: true }, '', window.location.href); + } + } + updateUrlFragment(relativePath) { if (!this.isHistoryManager || !relativePath) return; const newHash = '#page=' + relativePath; if (window.location.hash !== newHash) { // Update URL without page refresh - history.pushState({}, '', window.location.pathname + window.location.search + newHash); + history.pushState({ relativePath: relativePath }, '', window.location.pathname + window.location.search + newHash); } } @@ -149,9 +159,13 @@ if (fragment && this.initialSrc) { const absoluteUrl = this.buildAbsoluteUrl(fragment); this.iframe.src = absoluteUrl; - } else if (!fragment && this.initialSrc) { + } else if (!fragment) { // No fragment, navigate back to initial URL - this.iframe.src = this.initialSrc; + if (this.initialUrl) { + this.iframe.src = this.initialUrl; + } else if (this.initialSrc) { + this.iframe.src = this.initialSrc; + } } } @@ -244,12 +258,30 @@ handleIframeLoad() { this.isIframeLoaded = true; + // Dispatch custom event + this.dispatchEvent(new CustomEvent('iframe-loaded', { + detail: { iframe: this.iframe }, + bubbles: true + })); + } + + handleIframeMessage(event) { + // Simple message filtering - only accept messages from our iframe + if (this.iframe && event.source === this.iframe.contentWindow && event.data) { + if (event.data.type === 'height-update') { + this.updateIframeHeight(event.data.height); + } else if (event.data.type === 'url-change') { + this.handleUrlChange(event.data.url); + } + } + } + + handleUrlChange(currentUrl) { try { - const currentUrl = this.iframe.contentWindow.location.href; - if (!this.initialUrl) { - // First load - store the initial URL + // First load - store the initial URL and ensure history entry exists this.initialUrl = currentUrl; + this.ensureInitialHistoryEntry(); } else if (this.isHistoryManager && currentUrl !== this.initialUrl) { // Navigation detected - check if it's same domain if (currentUrl.startsWith(this.initialUrl) || @@ -263,25 +295,7 @@ } } } catch (e) { - // Handle potential cross-origin errors gracefully - console.warn('ABP Embedding: Could not access iframe URL due to cross-origin restrictions', e); - } - - // Dispatch custom event - this.dispatchEvent(new CustomEvent('iframe-loaded', { - detail: { iframe: this.iframe }, - bubbles: true - })); - } - - handleIframeMessage(event) { - // Simple message filtering - only accept height updates from our iframe - if (this.iframe && - event.source === this.iframe.contentWindow && - event.data && - event.data.type === 'height-update') { - - this.updateIframeHeight(event.data.height); + console.warn('ABP Embedding: Failed to handle URL change', e); } }