forked from EVOgeek/Vmeda.Online
		
	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.online": "message", | ||||
|   "core.openfile": "local_moodlemobileapp", | ||||
|   "core.openfilewithextension": "local_moodlemobileapp", | ||||
|   "core.openfullimage": "local_moodlemobileapp", | ||||
|   "core.openinbrowser": "local_moodlemobileapp", | ||||
|   "core.openinbrowserdescription": "local_moodlemobileapp", | ||||
|  | ||||
| @ -16,8 +16,6 @@ import { ElementController } from './ElementController'; | ||||
| 
 | ||||
| /** | ||||
|  * Possible types of frame elements. | ||||
|  * | ||||
|  * @todo Remove frame TAG support. | ||||
|  */ | ||||
| export type FrameElement = HTMLIFrameElement | HTMLObjectElement | HTMLEmbedElement; | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| <!-- 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. --> | ||||
| <!-- 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"> | ||||
|         <ion-button fill="clear" (click)="toggleFullscreen()" | ||||
|             [attr.aria-label]="(fullscreen ? 'core.disablefullscreen' : 'core.fullscreen') | translate"> | ||||
| @ -28,3 +28,8 @@ | ||||
|         {{ 'core.iframehelp' | translate }} | ||||
|     </ion-button> | ||||
| </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; | ||||
|     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(), | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -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<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); | ||||
| 
 | ||||
|             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. | ||||
|      * | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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<HTMLElement>('img, video, audio, source, track')); | ||||
|         const media = Array.from(element.querySelectorAll<HTMLElement>('img, video, audio, source, track, iframe, embed')); | ||||
|         media.forEach((media: HTMLElement) => { | ||||
|             const currentSrc = media.getAttribute('src'); | ||||
|             const newSrc = currentSrc ? | ||||
|  | ||||
| @ -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<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); | ||||
| @ -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.
 | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user