+
Programmatic Example
+
This example shows how to enable auto-height programmatically with custom options:
+
+
+
+
+
Status: Auto-height disabled
+
+
+
+
+
+
+
+
Example 1: Basic Iframe Embedding
+
This example shows a basic iframe embedding with data communication.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/package.json b/npm/packs/aspnetcore.mvc.ui.embedding/package.json
new file mode 100644
index 0000000000..4e2018cdab
--- /dev/null
+++ b/npm/packs/aspnetcore.mvc.ui.embedding/package.json
@@ -0,0 +1,35 @@
+{
+ "version": "9.3.0-rc.1",
+ "name": "@abp/aspnetcore.mvc.ui.embedding",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/abpframework/abp.git",
+ "directory": "npm/packs/aspnetcore.mvc.ui.embedding"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@abp/core": "~9.3.0-rc.1",
+ "@abp/utils": "~9.3.0-rc.1"
+ },
+ "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431",
+ "homepage": "https://abp.io",
+ "license": "LGPL-3.0",
+ "keywords": [
+ "aspnetcore",
+ "boilerplate",
+ "framework",
+ "web",
+ "best-practices",
+ "angular",
+ "maui",
+ "blazor",
+ "mvc",
+ "csharp",
+ "webapp",
+ "embedding",
+ "iframe",
+ "web-component"
+ ]
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000..1db934510a
--- /dev/null
+++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding-iframe.js
@@ -0,0 +1,331 @@
+(function() {
+ 'use strict';
+
+ // Check if we're in an iframe
+ if (window.self === window.top) {
+ return; // Not in iframe, exit
+ }
+
+ /**
+ * ABP Embedding Iframe Handler
+ * Simple one-way height reporting to parent
+ */
+ class AbpEmbeddingIframeHandler {
+ constructor() {
+ this.config = {
+ minHeight: 100,
+ maxHeight: 10000,
+ debounceDelay: 100
+ };
+ this.isEnabled = false;
+ 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', () => {
+ this.onDomReady();
+ });
+ } else {
+ this.onDomReady();
+ }
+ }
+
+ onDomReady() {
+ // Wait for content to load
+ if (document.readyState === 'complete') {
+ this.onPageLoaded();
+ } else {
+ window.addEventListener('load', () => {
+ this.onPageLoaded();
+ });
+ }
+ }
+
+ onPageLoaded() {
+ // Wait for layout completion then report height
+ this.waitForLayout().then(() => {
+ this.reportHeight();
+ });
+ }
+
+ async waitForLayout() {
+ // Wait for images to load
+ const images = document.querySelectorAll('img');
+ const imagePromises = Array.from(images).map(img => {
+ if (img.complete) return Promise.resolve();
+
+ return new Promise(resolve => {
+ const timeout = setTimeout(resolve, 2000); // Don't wait forever
+ img.addEventListener('load', () => { clearTimeout(timeout); resolve(); });
+ img.addEventListener('error', () => { clearTimeout(timeout); resolve(); });
+ });
+ });
+
+ await Promise.all(imagePromises);
+
+ // Wait for layout
+ await new Promise(resolve => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+
+ // Small delay for CSS/fonts
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ enable() {
+ if (this.isEnabled) return;
+
+ this.isEnabled = true;
+ this.setupMonitoring();
+ }
+
+ disable() {
+ this.isEnabled = false;
+
+ if (this.observer) {
+ this.observer.disconnect();
+ this.observer = null;
+ }
+
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = null;
+ }
+
+ window.removeEventListener('resize', this.debouncedReportHeight);
+
+ // Remove URL monitoring listeners
+ window.removeEventListener('popstate', this.handlePopState);
+ window.removeEventListener('hashchange', this.handleHashChange);
+ }
+
+ setupMonitoring() {
+ // Use MutationObserver for content changes
+ this.observer = new MutationObserver(this.onContentChange);
+ this.observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['style', 'class', 'height', 'width']
+ });
+
+ // Monitor window resize
+ window.addEventListener('resize', this.debouncedReportHeight);
+
+ // Monitor common dynamic content events
+ this.setupDynamicContentMonitoring();
+ }
+
+ setupDynamicContentMonitoring() {
+ // Monitor for images loading
+ const handleImageLoad = this.debouncedReportHeight;
+ document.addEventListener('load', handleImageLoad, true);
+ document.addEventListener('error', handleImageLoad, true);
+
+ // Monitor for AJAX/fetch requests
+ const originalFetch = window.fetch;
+ if (originalFetch) {
+ window.fetch = (...args) => {
+ return originalFetch.apply(this, args).then(response => {
+ setTimeout(this.debouncedReportHeight, 100);
+ return response;
+ });
+ };
+ }
+
+ // Monitor XMLHttpRequest
+ const originalXHROpen = XMLHttpRequest.prototype.open;
+ XMLHttpRequest.prototype.open = function(...args) {
+ this.addEventListener('loadend', handleImageLoad);
+ return originalXHROpen.apply(this, args);
+ };
+ }
+
+ onContentChange(mutations) {
+ let shouldUpdate = false;
+
+ for (const mutation of mutations) {
+ if (mutation.type === 'childList' &&
+ (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
+ shouldUpdate = true;
+ break;
+ } else if (mutation.type === 'attributes') {
+ shouldUpdate = true;
+ break;
+ }
+ }
+
+ if (shouldUpdate) {
+ this.debouncedReportHeight();
+ }
+ }
+
+ measureHeight() {
+ // Force layout calculation
+ document.body.offsetHeight;
+
+ // Try different height measurement methods
+ const heights = [
+ Math.max(document.body.scrollHeight, document.body.offsetHeight),
+ Math.max(document.documentElement.scrollHeight, document.documentElement.offsetHeight)
+ ];
+
+ // Get the maximum height
+ let maxHeight = Math.max(...heights);
+
+ if (maxHeight === 0) {
+ maxHeight = window.innerHeight || 400;
+ }
+
+ // Add small padding and ensure within bounds
+ maxHeight += 10;
+ return Math.max(
+ this.config.minHeight,
+ Math.min(maxHeight, this.config.maxHeight)
+ );
+ }
+
+ reportHeight() {
+ if (!this.isEnabled) return;
+
+ const height = this.measureHeight();
+ const threshold = this.lastReportedHeight === 0 ? 1 : 3;
+
+ if (Math.abs(height - this.lastReportedHeight) < threshold) {
+ return;
+ }
+
+ this.lastReportedHeight = height;
+
+ // Send height to parent
+ if (window.parent && window.parent !== window) {
+ window.parent.postMessage({
+ type: 'height-update',
+ height: height,
+ timestamp: Date.now()
+ }, '*');
+ }
+ }
+
+ debouncedReportHeight() {
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ }
+
+ this.debounceTimer = setTimeout(() => {
+ this.reportHeight();
+ 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
+ const handler = new AbpEmbeddingIframeHandler();
+ handler.init();
+
+ // Expose handler globally for manual control if needed
+ window.abpEmbeddingIframeHandler = handler;
+
+})();
\ No newline at end of file
diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css
new file mode 100644
index 0000000000..ce7feee4a1
--- /dev/null
+++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.css
@@ -0,0 +1,95 @@
+/* ABP Embedding Component Styles */
+
+abp-embedding {
+ display: block;
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+abp-embedding iframe {
+ border: none !important;
+ border-width: 0 !important;
+ border-style: none !important;
+ border-color: transparent !important;
+ outline: none !important;
+ outline-width: 0 !important;
+ outline-style: none !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: transparent !important;
+ background-color: transparent !important;
+ display: block;
+ box-shadow: none !important;
+ -webkit-box-shadow: none !important;
+ -moz-box-shadow: none !important;
+ filter: none !important;
+ -webkit-filter: none !important;
+ -moz-filter: none !important;
+ -webkit-appearance: none !important;
+ -moz-appearance: none !important;
+ appearance: none !important;
+ text-shadow: none !important;
+ -webkit-text-shadow: none !important;
+ -moz-text-shadow: none !important;
+ width: 100%;
+ min-height: 50vh; /* Minimum height for better UX */
+}
+
+/* Responsive behavior */
+abp-embedding.responsive {
+ aspect-ratio: 16 / 9;
+}
+
+abp-embedding.responsive iframe {
+ width: 100%;
+}
+
+/* Loading state */
+abp-embedding[loading]::before {
+ content: 'Loading...';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: #666;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 14px;
+ z-index: 1;
+}
+
+abp-embedding[loading] iframe {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+abp-embedding.loaded iframe {
+ opacity: 1;
+}
+
+/* Custom styling variables for easy theming */
+abp-embedding {
+ --border-radius: 0;
+ --box-shadow: none;
+ --background-color: transparent;
+}
+
+abp-embedding iframe {
+ border-radius: var(--border-radius) !important;
+ box-shadow: var(--box-shadow) !important;
+ background-color: var(--background-color) !important;
+}
+
+/* Auto-height specific styles - only non-height related rules */
+abp-embedding[auto-height] {
+ /* Let JavaScript handle all height management */
+}
+
+abp-embedding[auto-height] iframe {
+ /* Let JavaScript handle all height management */
+}
+
+/* Disable transitions during height updates to prevent conflicts */
+abp-embedding[auto-height].height-updating iframe {
+ transition: none !important;
+}
\ No newline at end of file
diff --git a/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js
new file mode 100644
index 0000000000..7dd6da03b0
--- /dev/null
+++ b/npm/packs/aspnetcore.mvc.ui.embedding/src/abp-embedding.js
@@ -0,0 +1,405 @@
+(function() {
+ 'use strict';
+
+ // Extend abp namespace if it exists
+ var abp = window.abp || {};
+ if (!window.abp) {
+ window.abp = abp;
+ }
+
+ abp.embedding = abp.embedding || {};
+
+ // Static state to ensure only one instance manages history
+ let historyManagerInstance = null;
+
+ /**
+ * ABP Embedding Web Component
+ * Simple iframe with auto-height capability
+ */
+ class AbpEmbeddingElement extends HTMLElement {
+ constructor() {
+ super();
+ this.iframe = null;
+ this.isIframeLoaded = false;
+ this.autoHeightConfig = null;
+ this.isHistoryManager = false;
+ this.initialSrc = null; // Original src attribute
+ this.initialUrl = null; // Actual loaded URL
+
+ // Bind methods
+ this.handleIframeLoad = this.handleIframeLoad.bind(this);
+ this.handleIframeMessage = this.handleIframeMessage.bind(this);
+ this.handlePopState = this.handlePopState.bind(this);
+ }
+
+ static get observedAttributes() {
+ return ['src', 'width', 'height', 'allow', 'sandbox', 'loading', 'auto-height'];
+ }
+
+ connectedCallback() {
+ this.setupHistoryManager();
+ this.render();
+ this.setupEventListeners();
+ this.handleInitialFragment();
+ }
+
+ disconnectedCallback() {
+ this.cleanup();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue !== newValue) {
+ this.updateIframeAttribute(name, newValue);
+ }
+ }
+
+ setupHistoryManager() {
+ if (historyManagerInstance === null) {
+ historyManagerInstance = this;
+ this.isHistoryManager = true;
+ } else {
+ console.warn('ABP Embedding: Multiple instances detected. Only the first instance will manage browser history.');
+ }
+ }
+
+ handleInitialFragment() {
+ if (!this.isHistoryManager) return;
+
+ // Store the original src attribute as initialSrc
+ this.initialSrc = this.getAttribute('src');
+
+ // Check if there's a fragment in the current URL
+ const fragment = this.parseFragmentFromUrl();
+ if (fragment && this.initialSrc) {
+ // Build absolute URL from relative fragment
+ const absoluteUrl = this.buildAbsoluteUrl(fragment);
+ // Update iframe src to navigate to the fragment URL
+ this.iframe.src = absoluteUrl;
+ }
+ }
+
+ parseFragmentFromUrl() {
+ const hash = window.location.hash;
+ if (hash && hash.startsWith('#page=')) {
+ return hash.substring(6); // Remove '#page=' prefix
+ }
+ return null;
+ }
+
+ buildAbsoluteUrl(relativePath) {
+ if (!this.initialSrc) return relativePath;
+
+ try {
+ // If relativePath starts with '/', it's relative to the domain
+ if (relativePath.startsWith('/')) {
+ const url = new URL(this.initialSrc);
+ return url.origin + relativePath;
+ } else {
+ // Otherwise, it's relative to the current path
+ return new URL(relativePath, this.initialSrc).href;
+ }
+ } catch (e) {
+ console.warn('ABP Embedding: Failed to build absolute URL', e);
+ return relativePath;
+ }
+ }
+
+ buildRelativePath(absoluteUrl) {
+ if (!this.initialUrl || !absoluteUrl) return null;
+
+ try {
+ const initialUrlObj = new URL(this.initialUrl);
+ const currentUrlObj = new URL(absoluteUrl);
+
+ // Check if same origin
+ if (initialUrlObj.origin !== currentUrlObj.origin) {
+ return null;
+ }
+
+ // Return pathname + search + hash relative to initial URL
+ const relativePath = currentUrlObj.pathname + currentUrlObj.search + currentUrlObj.hash;
+ const initialPath = initialUrlObj.pathname;
+
+ // If it's the same as initial path, return relative
+ if (relativePath === initialPath) {
+ return '/';
+ }
+
+ return relativePath;
+ } catch (e) {
+ console.warn('ABP Embedding: Failed to build relative path', e);
+ return null;
+ }
+ }
+
+ 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({ relativePath: relativePath }, '', window.location.pathname + window.location.search + newHash);
+ }
+ }
+
+ handlePopState(event) {
+ if (!this.isHistoryManager || !this.iframe) return;
+
+ const fragment = this.parseFragmentFromUrl();
+ if (fragment && this.initialSrc) {
+ const absoluteUrl = this.buildAbsoluteUrl(fragment);
+ this.iframe.src = absoluteUrl;
+ } else if (!fragment) {
+ // No fragment, navigate back to initial URL
+ if (this.initialUrl) {
+ this.iframe.src = this.initialUrl;
+ } else if (this.initialSrc) {
+ this.iframe.src = this.initialSrc;
+ }
+ }
+ }
+
+ render() {
+ // Clear existing content
+ this.innerHTML = '';
+
+ // Create iframe element
+ this.iframe = document.createElement('iframe');
+
+ // Set default attributes
+ this.iframe.setAttribute('frameborder', '0');
+ this.iframe.setAttribute('scrolling', 'auto');
+ this.iframe.style.border = 'none';
+ this.iframe.style.outline = 'none';
+ this.iframe.style.display = 'block';
+ this.iframe.style.width = '100%';
+
+ // Apply custom attributes
+ this.updateAllAttributes();
+
+ // Add load event listener
+ this.iframe.addEventListener('load', this.handleIframeLoad);
+
+ this.appendChild(this.iframe);
+ }
+
+ updateAllAttributes() {
+ const attributes = ['src', 'width', 'height', 'allow', 'sandbox', 'loading'];
+ attributes.forEach(attr => {
+ const value = this.getAttribute(attr);
+ if (value) {
+ this.updateIframeAttribute(attr, value);
+ }
+ });
+ }
+
+ updateIframeAttribute(name, value) {
+ if (!this.iframe) return;
+
+ switch (name) {
+ case 'src':
+ this.iframe.src = value;
+ this.isIframeLoaded = false;
+ break;
+ case 'width':
+ this.iframe.style.width = value.includes('%') || value.includes('px') ? value : value + 'px';
+ break;
+ case 'height':
+ if (!this.hasAttribute('auto-height')) {
+ this.iframe.style.height = value.includes('%') || value.includes('px') ? value : value + 'px';
+ }
+ break;
+ case 'auto-height':
+ if (value === 'true' || value === '') {
+ this.enableAutoHeight();
+ } else {
+ this.disableAutoHeight();
+ }
+ break;
+ default:
+ this.iframe.setAttribute(name, value);
+ }
+ }
+
+ setupEventListeners() {
+ // Listen for messages from iframe
+ window.addEventListener('message', this.handleIframeMessage);
+
+ // Listen for popstate events (back/forward buttons)
+ if (this.isHistoryManager) {
+ window.addEventListener('popstate', this.handlePopState);
+ }
+ }
+
+ cleanup() {
+ window.removeEventListener('message', this.handleIframeMessage);
+ if (this.isHistoryManager) {
+ window.removeEventListener('popstate', this.handlePopState);
+ // Reset the static instance if this was the history manager
+ if (historyManagerInstance === this) {
+ historyManagerInstance = null;
+ }
+ }
+ if (this.iframe) {
+ this.iframe.removeEventListener('load', this.handleIframeLoad);
+ }
+ }
+
+ 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 {
+ if (!this.initialUrl) {
+ // 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) ||
+ (this.initialSrc && currentUrl.startsWith(new URL(this.initialSrc).origin))) {
+
+ // Generate relative path and update URL fragment
+ const relativePath = this.buildRelativePath(currentUrl);
+ if (relativePath) {
+ this.updateUrlFragment(relativePath);
+ }
+ }
+ }
+ } catch (e) {
+ console.warn('ABP Embedding: Failed to handle URL change', e);
+ }
+ }
+
+ /**
+ * Update iframe height based on content
+ */
+ updateIframeHeight(height) {
+ if (!this.iframe || !height || height <= 0) {
+ return;
+ }
+
+ const config = this.autoHeightConfig || {};
+ const minHeight = config.minHeight || 100;
+ const maxHeight = config.maxHeight || window.innerHeight * 0.9;
+
+ // Ensure height is within reasonable bounds
+ const newHeight = Math.max(minHeight, Math.min(height, maxHeight));
+
+ // Update iframe height
+ this.iframe.style.height = newHeight + 'px';
+
+ // Dispatch custom event
+ this.dispatchEvent(new CustomEvent('height-updated', {
+ detail: {
+ height: newHeight,
+ originalHeight: height
+ },
+ bubbles: true
+ }));
+ }
+
+ /**
+ * Get the iframe element
+ */
+ getIframe() {
+ return this.iframe;
+ }
+
+ /**
+ * Check if iframe is loaded
+ */
+ isLoaded() {
+ return this.isIframeLoaded;
+ }
+
+ /**
+ * Enable auto-height functionality
+ */
+ enableAutoHeight(options = {}) {
+ const config = {
+ minHeight: options.minHeight || 100,
+ maxHeight: options.maxHeight || window.innerHeight * 0.9,
+ ...options
+ };
+
+ this.autoHeightConfig = config;
+ this.setAttribute('auto-height', 'true');
+ }
+
+ /**
+ * Disable auto-height functionality
+ */
+ disableAutoHeight() {
+ this.removeAttribute('auto-height');
+ this.autoHeightConfig = null;
+
+ // Reset to original height if specified
+ const originalHeight = this.getAttribute('height') || '400px';
+ if (this.iframe) {
+ this.iframe.style.height = originalHeight;
+ }
+ }
+ }
+
+ // Register the custom element
+ if (!customElements.get('abp-embedding')) {
+ customElements.define('abp-embedding', AbpEmbeddingElement);
+ }
+
+ // Add utility functions to abp.embedding namespace
+ abp.embedding.create = function(options) {
+ const element = document.createElement('abp-embedding');
+
+ if (options.src) element.setAttribute('src', options.src);
+ if (options.width) element.setAttribute('width', options.width);
+ if (options.height) element.setAttribute('height', options.height);
+ if (options.allow) element.setAttribute('allow', options.allow);
+ if (options.sandbox) element.setAttribute('sandbox', options.sandbox);
+ if (options.loading) element.setAttribute('loading', options.loading);
+ if (options.autoHeight) element.setAttribute('auto-height', options.autoHeight);
+
+ return element;
+ };
+
+ abp.embedding.enableAutoHeight = function(element, options) {
+ if (element && typeof element.enableAutoHeight === 'function') {
+ element.enableAutoHeight(options);
+ }
+ };
+
+ abp.embedding.disableAutoHeight = function(element) {
+ if (element && typeof element.disableAutoHeight === 'function') {
+ element.disableAutoHeight();
+ }
+ };
+
+})();
\ No newline at end of file
diff --git a/nupkg/common.ps1 b/nupkg/common.ps1
index e916a379d2..f0d3b860ec 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -125,6 +125,7 @@ $projects = (
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI",
+ "framework/src/Volo.Abp.AspNetCore.Mvc.UI.Embedding",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared",