commit
0de0394a66
Binary file not shown.
|
@ -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.
|
||||
};
|
||||
|
|
|
@ -1230,6 +1230,8 @@ export class CoreUtilsProvider {
|
|||
type: CoreAnalyticsEventType.OPEN_LINK,
|
||||
link: CoreUrlUtils.unfixPluginfileURL(url),
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.error('Error opening online file ' + url + ' with mimetype ' + mimetype);
|
||||
this.logger.error('Error: ', JSON.stringify(error));
|
||||
|
|
|
@ -11,17 +11,17 @@ Feature: It opens files properly.
|
|||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
|
||||
@lms_from3.10
|
||||
Scenario: Open a file
|
||||
Given the following "activities" exist:
|
||||
| activity | name | intro | display | course | defaultfilename |
|
||||
| resource | Test TXT | Test TXT description | 5 | C1 | A txt.txt |
|
||||
| resource | Test RTF | Test RTF description | 5 | C1 | A rtf.rtf |
|
||||
| resource | Test DOC | Test DOC description | 5 | C1 | A doc.doc |
|
||||
And the following config values are set as admin:
|
||||
| filetypeexclusionlist | rtf,doc | tool_mobile |
|
||||
|
||||
@lms_from3.10
|
||||
Scenario: Open a file
|
||||
Given I entered the resource activity "Test TXT" on course "Course 1" as "student1" in the app
|
||||
And I entered the resource activity "Test TXT" on course "Course 1" as "student1" in the app
|
||||
When I press "Open" in the app
|
||||
Then the app should have opened a browser tab with url "^blob:"
|
||||
|
||||
|
@ -57,3 +57,44 @@ Feature: It opens files properly.
|
|||
And I press "Test DOC" in the app
|
||||
And I press "Open" in the app
|
||||
Then I should find "This file may not work as expected on this device" in the app
|
||||
|
||||
Scenario: Open a PDF embedded using an iframe
|
||||
# Using http://webserver directly because $WWWROOT cannot be used in generators or when creating the page manually.
|
||||
Given the following "activities" exist:
|
||||
| activity | idnumber | course | name | content |
|
||||
| page | page1 | C1 | Page with embedded PDF | <iframe src="http://webserver/local/moodleappbehat/fixtures/dummy.pdf" width="100%" height="500"></iframe> |
|
||||
| page | page2 | C1 | Page with embedded web | <iframe src="https://moodle.org" width="100%" height="500" data-open-external="true"></iframe> |
|
||||
And the following config values are set as admin:
|
||||
| custommenuitems | PDF item\|http://webserver/local/moodleappbehat/fixtures/dummy.pdf\|embedded | tool_mobile |
|
||||
And I entered the course "Course 1" as "student1" in the app
|
||||
|
||||
When I press "Page with embedded PDF" in the app
|
||||
And I press "Open PDF file" in the app
|
||||
Then the app should have opened a browser tab with url "dummy.pdf"
|
||||
|
||||
When I close the browser tab opened by the app
|
||||
And I press the back button in the app
|
||||
And I press "Page with embedded web" in the app
|
||||
And I press "Open in browser" in the app
|
||||
And I press "OK" in the app
|
||||
Then the app should have opened a browser tab with url "moodle.org"
|
||||
|
||||
When I close the browser tab opened by the app
|
||||
And I switch network connection to offline
|
||||
And I press "Open in browser" in the app
|
||||
Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app
|
||||
|
||||
When I press "OK" in the app
|
||||
And I press the back button in the app
|
||||
And I press "Page with embedded PDF" in the app
|
||||
And I press "Open PDF file" in the app
|
||||
Then the app should have opened a browser tab with url "^blob"
|
||||
|
||||
When I close the browser tab opened by the app
|
||||
When I switch network connection to wifi
|
||||
And I press the back button in the app
|
||||
And I press the back button in the app
|
||||
And I press "More" in the app
|
||||
And I press "PDF item" in the app
|
||||
And I press "Open PDF file" in the app
|
||||
Then the app should have opened a browser tab with url "dummy.pdf"
|
||||
|
|
Loading…
Reference in New Issue