diff --git a/src/app/core/mainmenu/pages/more/more.html b/src/app/core/mainmenu/pages/more/more.html index c349498a4..206208638 100644 --- a/src/app/core/mainmenu/pages/more/more.html +++ b/src/app/core/mainmenu/pages/more/more.html @@ -30,7 +30,7 @@
- +

{{item.label}}

diff --git a/src/app/directives/directives.module.ts b/src/app/directives/directives.module.ts index 0d389ce22..40c292f36 100644 --- a/src/app/directives/directives.module.ts +++ b/src/app/directives/directives.module.ts @@ -16,16 +16,18 @@ import { NgModule } from '@angular/core'; import { CoreAutoFocusDirective } from './auto-focus'; import { CoreExternalContentDirective } from './external-content'; +import { CoreFabDirective } from './fab'; import { CoreFormatTextDirective } from './format-text'; +import { CoreLinkDirective } from './link'; import { CoreLongPressDirective } from './long-press'; import { CoreSupressEventsDirective } from './supress-events'; -import { CoreFabDirective } from './fab'; @NgModule({ declarations: [ CoreAutoFocusDirective, CoreExternalContentDirective, CoreFormatTextDirective, + CoreLinkDirective, CoreLongPressDirective, CoreSupressEventsDirective, CoreFabDirective, @@ -35,6 +37,7 @@ import { CoreFabDirective } from './fab'; CoreAutoFocusDirective, CoreExternalContentDirective, CoreFormatTextDirective, + CoreLinkDirective, CoreLongPressDirective, CoreSupressEventsDirective, CoreFabDirective, diff --git a/src/app/directives/format-text.ts b/src/app/directives/format-text.ts index 56ac42cee..da1baf562 100644 --- a/src/app/directives/format-text.ts +++ b/src/app/directives/format-text.ts @@ -24,6 +24,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreSite } from '@classes/site'; import { Translate } from '@singletons/core.singletons'; import { CoreExternalContentDirective } from './external-content'; +import { CoreLinkDirective } from './link'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -454,7 +455,9 @@ export class CoreFormatTextDirective implements OnChanges { // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. - // @todo + const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content); + linkDir.capture = true; + linkDir.ngOnInit(); this.addExternalContent(anchor); }); diff --git a/src/app/directives/link.ts b/src/app/directives/link.ts new file mode 100644 index 000000000..c220ffc39 --- /dev/null +++ b/src/app/directives/link.ts @@ -0,0 +1,198 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; +import { IonContent } from '@ionic/angular'; + +import { CoreFileHelper } from '@services/file-helper'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreConstants } from '@core/constants'; + +/** + * Directive to open a link in external browser or in the app. + */ +@Directive({ + selector: '[core-link]', +}) +export class CoreLinkDirective implements OnInit { + + @Input() capture?: boolean | string; // If the link needs to be captured by the app. + @Input() inApp?: boolean | string; // True to open in embedded browser, false to open in system browser. + /* Whether the link should be opened with auto-login. Accepts the following values: + "yes" -> Always auto-login. + "no" -> Never auto-login. + "check" -> Auto-login only if it points to the current site. Default value. */ + @Input() autoLogin = 'check'; + + protected element: Element; + + constructor( + element: ElementRef, + @Optional() protected content: IonContent, + ) { + this.element = element.nativeElement; + } + + /** + * Function executed when the component is initialized. + */ + ngOnInit(): void { + this.inApp = typeof this.inApp == 'undefined' ? this.inApp : CoreUtils.instance.isTrueOrOne(this.inApp); + + // @todo: Handle split view? + + this.element.addEventListener('click', (event) => { + if (event.defaultPrevented) { + return; // Link already treated, stop. + } + + let href = this.element.getAttribute('href') || this.element.getAttribute('ng-reflect-href') || + this.element.getAttribute('xlink:href'); + + if (!href || CoreUrlUtils.instance.getUrlScheme(href) == 'javascript') { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const openIn = this.element.getAttribute('data-open-in'); + + if (CoreUtils.instance.isTrueOrOne(this.capture)) { + href = CoreTextUtils.instance.decodeURI(href); + + // @todo: Handle link. + this.navigate(href, openIn); + } else { + this.navigate(href, openIn); + } + }); + } + + /** + * Convenience function to correctly navigate, open file or url in the browser. + * + * @param href HREF to be opened. + * @param openIn Open In App value coming from data-open-in attribute. + * @return Promise resolved when done. + */ + protected async navigate(href: string, openIn?: string | null): Promise { + + if (CoreUrlUtils.instance.isLocalFileUrl(href)) { + return this.openLocalFile(href); + } + + if (href.charAt(0) == '#') { + // Look for id or name. + href = href.substr(1); + CoreDomUtils.instance.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']'); + + return; + } + + // @todo: Custom URL schemes. + + return this.openExternalLink(href, openIn); + } + + /** + * Open a local file. + * + * @param path Path to the file. + * @return Promise resolved when done. + */ + protected async openLocalFile(path: string): Promise { + const filename = path.substr(path.lastIndexOf('/') + 1); + + if (!CoreFileHelper.instance.isOpenableInApp({ filename })) { + try { + await CoreFileHelper.instance.showConfirmOpenUnsupportedFile(); + } catch (error) { + return; // Cancelled, stop. + } + } + + try { + await CoreUtils.instance.openFile(path); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + } + } + + /** + * Open an external link in the app or in browser. + * + * @param href HREF to be opened. + * @param openIn Open In App value coming from data-open-in attribute. + * @return Promise resolved when done. + */ + protected async openExternalLink(href: string, openIn?: string | null): Promise { + // It's an external link, we will open with browser. Check if we need to auto-login. + if (!CoreSites.instance.isLoggedIn()) { + // Not logged in, cannot auto-login. + if (this.inApp) { + CoreUtils.instance.openInApp(href); + } else { + CoreUtils.instance.openInBrowser(href); + } + + return; + } + + // Check if URL does not have any protocol, so it's a relative URL. + if (!CoreUrlUtils.instance.isAbsoluteURL(href)) { + // Add the site URL at the begining. + if (href.charAt(0) == '/') { + href = CoreSites.instance.getCurrentSite()!.getURL() + href; + } else { + href = CoreSites.instance.getCurrentSite()!.getURL() + '/' + href; + } + } + + if (this.autoLogin == 'yes') { + if (this.inApp) { + await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLogin(href); + } else { + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLogin(href); + } + } else if (this.autoLogin == 'no') { + if (this.inApp) { + CoreUtils.instance.openInApp(href); + } else { + CoreUtils.instance.openInBrowser(href); + } + } else { + // Priority order is: core-link inApp attribute > forceOpenLinksIn setting > data-open-in HTML attribute. + let openInApp = this.inApp; + if (typeof this.inApp == 'undefined') { + if (CoreConstants.CONFIG.forceOpenLinksIn == 'browser') { + openInApp = false; + } else if (CoreConstants.CONFIG.forceOpenLinksIn == 'app' || openIn == 'app') { + openInApp = true; + } + } + + if (openInApp) { + await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLoginIfSameSite(href); + } else { + await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(href); + } + } + } + +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index fe9f98a26..1e05ea6d4 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -66,6 +66,7 @@ declare global { appstores: Record; displayqroncredentialscreen?: boolean; displayqronsitescreen?: boolean; + forceOpenLinksIn: 'app' | 'browser'; }; BUILD: {