diff --git a/scripts/langindex.json b/scripts/langindex.json index c9c894df62f..dde73d5321c 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 8b9d335376d..a3e51d64592 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 22c8fabeb35..cb54e339311 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 4c49d610ee0..3006e7f7862 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 2ef09a0eb9c..0baf46f0185 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 ca0e0007abe..e6518b69db1 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 d44738c6e16..bb986499232 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 5a2db963ab9..0410a29899e 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. +};