MOBILE-4173 iframe: Launch PDF and some iframes in external app
This commit is contained in:
		
							parent
							
								
									1143d4d695
								
							
						
					
					
						commit
						327fe019a4
					
				| @ -2231,6 +2231,7 @@ | |||||||
|   "core.ok": "moodle", |   "core.ok": "moodle", | ||||||
|   "core.online": "message", |   "core.online": "message", | ||||||
|   "core.openfile": "local_moodlemobileapp", |   "core.openfile": "local_moodlemobileapp", | ||||||
|  |   "core.openfilewithextension": "local_moodlemobileapp", | ||||||
|   "core.openfullimage": "local_moodlemobileapp", |   "core.openfullimage": "local_moodlemobileapp", | ||||||
|   "core.openinbrowser": "local_moodlemobileapp", |   "core.openinbrowser": "local_moodlemobileapp", | ||||||
|   "core.openinbrowserdescription": "local_moodlemobileapp", |   "core.openinbrowserdescription": "local_moodlemobileapp", | ||||||
|  | |||||||
| @ -16,8 +16,6 @@ import { ElementController } from './ElementController'; | |||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Possible types of frame elements. |  * Possible types of frame elements. | ||||||
|  * |  | ||||||
|  * @todo Remove frame TAG support. |  | ||||||
|  */ |  */ | ||||||
| export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement; | export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| <!-- Display a loading until the iframe page is loaded. --> | <!-- Display a loading until the iframe page is loaded. --> | ||||||
| <core-loading [hideUntil]="!loading && safeUrl" /> | <core-loading [hideUntil]="!loading && (safeUrl || launchExternalLabel)" /> | ||||||
| 
 | 
 | ||||||
| <!--The iframe needs to be outside of core-loading, otherwise the request is canceled while loading. --> | <!--The iframe needs to be outside of core-loading, otherwise the request is canceled while loading. --> | ||||||
| <!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. --> | <!-- Don't add the iframe until safeUrl is set, adding an iframe with null as src causes the iframe to load the whole app. --> | ||||||
| <ng-container *ngIf="safeUrl"> | <ng-container *ngIf="safeUrl && !launchExternalLabel"> | ||||||
|     <core-navbar-buttons slot="end" prepend *ngIf="initialized && showFullscreenOnToolbar && !loading"> |     <core-navbar-buttons slot="end" prepend *ngIf="initialized && showFullscreenOnToolbar && !loading"> | ||||||
|         <ion-button fill="clear" (click)="toggleFullscreen()" |         <ion-button fill="clear" (click)="toggleFullscreen()" | ||||||
|             [attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate"> |             [attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate"> | ||||||
| @ -28,3 +28,8 @@ | |||||||
|         {{ 'core.iframehelp' | translate }} |         {{ 'core.iframehelp' | translate }} | ||||||
|     </ion-button> |     </ion-button> | ||||||
| </ng-container> | </ng-container> | ||||||
|  | 
 | ||||||
|  | <!-- Iframe content needs to be launched in an external app. --> | ||||||
|  | <ion-button *ngIf="launchExternalLabel" expand="block" class="ion-text-wrap ion-margin" (click)="launchExternal()"> | ||||||
|  |     {{ launchExternalLabel }} | ||||||
|  | </ion-button> | ||||||
|  | |||||||
| @ -55,6 +55,7 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { | |||||||
|     safeUrl?: SafeResourceUrl; |     safeUrl?: SafeResourceUrl; | ||||||
|     displayHelp = false; |     displayHelp = false; | ||||||
|     fullscreen = false; |     fullscreen = false; | ||||||
|  |     launchExternalLabel?: string; // Text to set to the button to launch external app.
 | ||||||
| 
 | 
 | ||||||
|     initialized = false; |     initialized = false; | ||||||
| 
 | 
 | ||||||
| @ -173,6 +174,19 @@ export class CoreIframeComponent implements OnChanges, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         let url = this.src; |         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)) { |         if (url && !CoreUrlUtils.isLocalFileUrl(url)) { | ||||||
|             url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url; |             url = CoreUrlUtils.getYoutubeEmbedUrl(url) || url; | ||||||
|             this.displayHelp = CoreIframeUtils.shouldDisplayHelpForUrl(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(), | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -523,10 +523,15 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const iframeControllers = iframes.map(iframe => { |         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)); |             promises.push(this.treatIframe(iframe, site)); | ||||||
| 
 | 
 | ||||||
|             return new FrameElementController(iframe, !this.disabled); |             return new FrameElementController(iframe, !this.disabled); | ||||||
|         }); |         }).filter((controller): controller is FrameElementController => controller !== undefined); | ||||||
| 
 | 
 | ||||||
|         svgImages.forEach((image) => { |         svgImages.forEach((image) => { | ||||||
|             this.addExternalContent(image); |             this.addExternalContent(image); | ||||||
| @ -562,11 +567,16 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // Handle all kind of frames.
 |         // Handle all kind of frames.
 | ||||||
|         const frameControllers = frames.map<FrameElementController>((frame) => { |         const frameControllers = frames.map((frame) => { | ||||||
|  |             const { launchExternal, label } = CoreIframeUtils.frameShouldLaunchExternal(frame); | ||||||
|  |             if (launchExternal && this.replaceFrameWithButton(frame, site, label)) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             CoreIframeUtils.treatFrame(frame, false); |             CoreIframeUtils.treatFrame(frame, false); | ||||||
| 
 | 
 | ||||||
|             return new FrameElementController(frame, !this.disabled); |             return new FrameElementController(frame, !this.disabled); | ||||||
|         }); |         }).filter((controller): controller is FrameElementController => controller !== undefined); | ||||||
| 
 | 
 | ||||||
|         CoreDomUtils.handleBootstrapTooltips(div); |         CoreDomUtils.handleBootstrapTooltips(div); | ||||||
| 
 | 
 | ||||||
| @ -863,6 +873,38 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec | |||||||
|         CoreIframeUtils.treatFrame(iframe, false); |         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. |      * Add iframe help option. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -233,6 +233,7 @@ | |||||||
|     "ok": "OK", |     "ok": "OK", | ||||||
|     "online": "Online", |     "online": "Online", | ||||||
|     "openfile": "Open file", |     "openfile": "Open file", | ||||||
|  |     "openfilewithextension": "Open {{extension}} file", | ||||||
|     "openfullimage": "Click here to display the full size image", |     "openfullimage": "Click here to display the full size image", | ||||||
|     "openinbrowser": "Open in browser", |     "openinbrowser": "Open in browser", | ||||||
|     "openinbrowserdescription": "You will be taken to a web browser", |     "openinbrowserdescription": "You will be taken to a web browser", | ||||||
|  | |||||||
| @ -687,7 +687,7 @@ export class CoreDomUtilsProvider { | |||||||
|         const element = this.convertToElement(html); |         const element = this.convertToElement(html); | ||||||
| 
 | 
 | ||||||
|         // Treat elements with src (img, audio, video, ...).
 |         // Treat elements with src (img, audio, video, ...).
 | ||||||
|         const media = Array.from(element.querySelectorAll<HTMLElement>('img, video, audio, source, track')); |         const media = Array.from(element.querySelectorAll<HTMLElement>('img, video, audio, source, track, iframe, embed')); | ||||||
|         media.forEach((media: HTMLElement) => { |         media.forEach((media: HTMLElement) => { | ||||||
|             const currentSrc = media.getAttribute('src'); |             const currentSrc = media.getAttribute('src'); | ||||||
|             const newSrc = currentSrc ? |             const newSrc = currentSrc ? | ||||||
|  | |||||||
| @ -33,6 +33,9 @@ import { CorePath } from '@singletons/path'; | |||||||
| import { CorePromisedValue } from '@classes/promised-value'; | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
| import { CorePlatform } from '@services/platform'; | import { CorePlatform } from '@services/platform'; | ||||||
| import { FrameElement } from '@classes/element-controllers/FrameElementController'; | 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 & { | type CoreFrameElement = FrameElement & { | ||||||
|     window?: Window; |     window?: Window; | ||||||
| @ -45,7 +48,7 @@ type CoreFrameElement = FrameElement & { | |||||||
| @Injectable({ providedIn: 'root' }) | @Injectable({ providedIn: 'root' }) | ||||||
| export class CoreIframeUtilsProvider { | export class CoreIframeUtilsProvider { | ||||||
| 
 | 
 | ||||||
|     static readonly FRAME_TAGS = ['iframe', 'frame', 'object', 'embed']; |     static readonly FRAME_TAGS = ['iframe', 'object', 'embed']; | ||||||
| 
 | 
 | ||||||
|     protected logger: CoreLogger; |     protected logger: CoreLogger; | ||||||
|     protected waitAutoLoginDefer?: CorePromisedValue<void>; |     protected waitAutoLoginDefer?: CorePromisedValue<void>; | ||||||
| @ -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<void> { | ||||||
|  |         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); | export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider); | ||||||
| @ -641,3 +722,12 @@ export const CoreIframeUtils = makeSingleton(CoreIframeUtilsProvider); | |||||||
| type CoreIframeHTMLAnchorElement = HTMLAnchorElement & { | type CoreIframeHTMLAnchorElement = HTMLAnchorElement & { | ||||||
|     treated?: boolean; // Whether the element has been treated already.
 |     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.
 | ||||||
|  | }; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user