diff --git a/scripts/langindex.json b/scripts/langindex.json
index c9c894df6..dde73d532 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -2231,6 +2231,7 @@
"core.ok": "moodle",
"core.online": "message",
"core.openfile": "local_moodlemobileapp",
+ "core.openfilewithextension": "local_moodlemobileapp",
"core.openfullimage": "local_moodlemobileapp",
"core.openinbrowser": "local_moodlemobileapp",
"core.openinbrowserdescription": "local_moodlemobileapp",
diff --git a/src/core/classes/element-controllers/FrameElementController.ts b/src/core/classes/element-controllers/FrameElementController.ts
index 8b9d33537..a3e51d645 100644
--- a/src/core/classes/element-controllers/FrameElementController.ts
+++ b/src/core/classes/element-controllers/FrameElementController.ts
@@ -16,8 +16,6 @@ import { ElementController } from './ElementController';
/**
* Possible types of frame elements.
- *
- * @todo Remove frame TAG support.
*/
export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement;
diff --git a/src/core/components/iframe/core-iframe.html b/src/core/components/iframe/core-iframe.html
index 22c8fabeb..cb54e3393 100644
--- a/src/core/components/iframe/core-iframe.html
+++ b/src/core/components/iframe/core-iframe.html
@@ -1,9 +1,9 @@
-
+
-
+
@@ -28,3 +28,8 @@
{{ 'core.iframehelp' | translate }}
+
+
+
+ {{ launchExternalLabel }}
+
diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts
index 4c49d610e..3006e7f78 100644
--- a/src/core/components/iframe/iframe.ts
+++ b/src/core/components/iframe/iframe.ts
@@ -55,6 +55,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
safeUrl?: SafeResourceUrl;
displayHelp = false;
fullscreen = false;
+ launchExternalLabel?: string; // Text to set to the button to launch external app.
initialized = false;
@@ -173,6 +174,19 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
let url = this.src;
+ if (url) {
+ const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(url);
+
+ if (launchExternal) {
+ this.launchExternalLabel = label;
+ this.loading = false;
+
+ return;
+ }
+ }
+
+ this.launchExternalLabel = undefined;
+
if (url && !CoreUrlUtils.isLocalFileUrl(url)) {
url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url;
this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(url);
@@ -260,4 +274,17 @@ export class CoreIframeComponent implements OnChanges, OnDestroy {
}
}
+ /**
+ * Launch content in an external app.
+ */
+ launchExternal(): void {
+ if (!this.src) {
+ return;
+ }
+
+ CoreIframeUtils.frameLaunchExternal(this.src, {
+ site: CoreSites.getCurrentSite(),
+ });
+ }
+
}
diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts
index 2ef09a0eb..0baf46f01 100644
--- a/src/core/directives/format-text.ts
+++ b/src/core/directives/format-text.ts
@@ -523,10 +523,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
});
const iframeControllers = iframes.map(iframe => {
+ const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(iframe);
+ if (launchExternal && this.replaceFrameWithButton(iframe, site, label)) {
+ return;
+ }
+
promises.push(this.treatIframe(iframe, site));
return new FrameElementController(iframe, !this.disabled);
- });
+ }).filter((controller): controller is FrameElementController => controller !== undefined);
svgImages.forEach((image) => {
this.addExternalContent(image);
@@ -562,11 +567,16 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
});
// Handle all kind of frames.
- const frameControllers = frames.map((frame) => {
+ const frameControllers = frames.map((frame) => {
+ const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(frame);
+ if (launchExternal && this.replaceFrameWithButton(frame, site, label)) {
+ return;
+ }
+
CoreIframeUtils.treatFrame(frame, false);
return new FrameElementController(frame, !this.disabled);
- });
+ }).filter((controller): controller is FrameElementController => controller !== undefined);
CoreDomUtils.handleBootstrapTooltips(div);
@@ -863,6 +873,38 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
CoreIframeUtils.treatFrame(iframe, false);
}
+ /**
+ * Replace a frame with a button to open the frame's URL in an external app.
+ *
+ * @param frame Frame element to replace.
+ * @param site Site instance.
+ * @param label The text to put in the button.
+ * @returns Whether iframe was replaced.
+ */
+ protected replaceFrameWithButton(frame: FrameElement, site: CoreSite | undefined, label: string): boolean {
+ const url = 'src' in frame ? frame.src : frame.data;
+ if (!url) {
+ return false;
+ }
+
+ const button = document.createElement('ion-button');
+ button.setAttribute('expand', 'block');
+ button.classList.add('ion-text-wrap');
+ button.innerHTML = label;
+
+ button.addEventListener('click', () => {
+ CoreIframeUtils.frameLaunchExternal(url, {
+ site,
+ component: this.component,
+ componentId: this.componentId,
+ });
+ });
+
+ frame.replaceWith(button);
+
+ return true;
+ }
+
/**
* Add iframe help option.
*
diff --git a/src/core/lang.json b/src/core/lang.json
index ca0e0007a..e6518b69d 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -233,6 +233,7 @@
"ok": "OK",
"online": "Online",
"openfile": "Open file",
+ "openfilewithextension": "Open {{extension}} file",
"openfullimage": "Click here to display the full size image",
"openinbrowser": "Open in browser",
"openinbrowserdescription": "You will be taken to a web browser",
diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index d44738c6e..bb9864992 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -687,7 +687,7 @@ export class CoreDomUtilsProvider {
const element = this.convertToElement(html);
// Treat elements with src (img, audio, video, ...).
- const media = Array.from(element.querySelectorAll('img, video, audio, source, track'));
+ const media = Array.from(element.querySelectorAll('img, video, audio, source, track, iframe, embed'));
media.forEach((media: HTMLElement) => {
const currentSrc = media.getAttribute('src');
const newSrc = currentSrc ?
diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts
index 5a2db963a..0410a2989 100644
--- a/src/core/services/utils/iframe.ts
+++ b/src/core/services/utils/iframe.ts
@@ -33,6 +33,9 @@ import { CorePath } from '@singletons/path';
import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform';
import { FrameElement } from '@classes/element-controllers/FrameElementController';
+import { CoreMimetypeUtils } from './mimetype';
+import { CoreFilepool } from '@services/filepool';
+import { CoreSite } from '@classes/sites/site';
type CoreFrameElement = FrameElement & {
window?: Window;
@@ -45,7 +48,7 @@ type CoreFrameElement = FrameElement & {
@Injectable({ providedIn: 'root' })
export class CoreIframeUtilsProvider {
- static readonly FRAME_TAGS = ['iframe', 'frame', 'object', 'embed'];
+ static readonly FRAME_TAGS = ['iframe', 'object', 'embed'];
protected logger: CoreLogger;
protected waitAutoLoginDefer?: CorePromisedValue;
@@ -631,6 +634,84 @@ export class CoreIframeUtilsProvider {
});
}
+ /**
+ * Check if a frame content should be opened with an external app (PDF reader, browser, etc.).
+ *
+ * @param urlOrFrame Either a URL of a frame, or the frame to check.
+ * @returns Whether it should be opened with an external app, and the label for the action to launch in external.
+ */
+ frameShouldLaunchExternal(urlOrFrame: string | FrameElement): { launchExternal: boolean; label: string } {
+ const url = typeof urlOrFrame === 'string' ?
+ urlOrFrame :
+ ('src' in urlOrFrame ? urlOrFrame.src : urlOrFrame.data);
+ const frame = typeof urlOrFrame !== 'string' && urlOrFrame;
+
+ const extension = url && CoreMimetypeUtils.guessExtensionFromUrl(url);
+ const launchExternal = extension === 'pdf' || (frame && frame.getAttribute('data-open-external') === 'true');
+
+ let label = '';
+ if (launchExternal) {
+ const mimetype = extension && CoreMimetypeUtils.getMimeType(extension);
+
+ label = mimetype && mimetype !== 'text/html' && mimetype !== 'text/plain' ?
+ Translate.instant('core.openfilewithextension', { extension: extension.toUpperCase() }) :
+ Translate.instant('core.openinbrowser');
+ }
+
+ return {
+ launchExternal,
+ label,
+ };
+ }
+
+ /**
+ * Launch a frame content in an external app.
+ *
+ * @param url Frame URL.
+ * @param options Options
+ */
+ async frameLaunchExternal(url: string, options: LaunchExternalOptions = {}): Promise {
+ const modal = await CoreDomUtils.showModalLoading();
+
+ try {
+ if (!CoreNetwork.isOnline()) {
+ // User is offline, try to open a local copy of the file if present.
+ const localUrl = options.site ?
+ await CoreUtils.ignoreErrors(CoreFilepool.getInternalUrlByUrl(options.site.getId(), url)) :
+ undefined;
+
+ if (localUrl) {
+ CoreUtils.openFile(localUrl);
+ } else {
+ CoreDomUtils.showErrorModal('core.networkerrormsg', true);
+ }
+
+ return;
+ }
+
+ const mimetype = await CoreUtils.ignoreErrors(CoreUtils.getMimeTypeFromUrl(url));
+
+ if (!mimetype || mimetype === 'text/html' || mimetype === 'text/plain') {
+ // It's probably a web page, open in browser.
+ options.site ? options.site.openInBrowserWithAutoLogin(url) : CoreUtils.openInBrowser(url);
+
+ return;
+ }
+
+ // Open the file using the online URL and try to download it in background for offline usage.
+ if (options.site) {
+ CoreFilepool.getUrlByUrl(options.site.getId(), url, options.component, options.componentId, 0, false);
+
+ url = await options.site.checkAndFixPluginfileURL(url);
+ }
+
+ CoreUtils.openOnlineFile(url);
+
+ } finally {
+ modal.dismiss();
+ }
+ }
+
}
export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider);
@@ -641,3 +722,12 @@ export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider);
type CoreIframeHTMLAnchorElement = HTMLAnchorElement & {
treated?: boolean; // Whether the element has been treated already.
};
+
+/**
+ * Options to pass to frameLaunchExternal.
+ */
+type LaunchExternalOptions = {
+ site?: CoreSite; // Site the frame belongs to.
+ component?: string; // Component to download the file if needed.
+ componentId?: string | number; // Component ID to use in conjunction with the component.
+};