diff --git a/src/core/directives/aria-button.ts b/src/core/directives/aria-button.ts index 092c53737..3d0d29593 100644 --- a/src/core/directives/aria-button.ts +++ b/src/core/directives/aria-button.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Directive, ElementRef, OnInit, Output, EventEmitter } from '@angular/core'; +import { CoreDom } from '@singletons/dom'; /** * Directive to emulate click and key actions following aria role button. @@ -36,22 +37,7 @@ export class CoreAriaButtonClickDirective implements OnInit { * Initialize actions. */ ngOnInit(): void { - this.element.addEventListener('click', async (event) => { - this.ariaButtonClick.emit(event); - }); - - this.element.addEventListener('keydown', async (event) => { - if ((event.key == ' ' || event.key == 'Enter')) { - event.preventDefault(); - event.stopPropagation(); - } - }); - - this.element.addEventListener('keyup', async (event) => { - if ((event.key == ' ' || event.key == 'Enter')) { - this.ariaButtonClick.emit(event); - } - }); + CoreDom.onActivate(this.element, (event) => this.ariaButtonClick.emit(event)); } } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index f134eddd6..7b4d8a7ea 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -452,9 +452,16 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo const svgImages = Array.from(div.querySelectorAll('image')); const promises: Promise[] = []; + this.treatAppUrlElements(div, site); + // Walk through the content to find the links and add our directive to it. // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { + if (anchor.getAttribute('data-app-url')) { + // Link already treated in data-app-url, ignore it. + return; + } + // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content); linkDir.capture = this.captureLinks ?? true; @@ -546,6 +553,57 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo await Promise.all(promises); } + /** + * Treat elements with an app-url data attribute. + * + * @param div Div containing the elements. + * @param site Site. + */ + protected treatAppUrlElements(div: HTMLElement, site?: CoreSite): void { + const appUrlElements = Array.from(div.querySelectorAll('*[data-app-url]')); + + appUrlElements.forEach((element) => { + const url = element.getAttribute('data-app-url'); + if (!url) { + return; + } + + if (element.tagName !== 'BUTTON' && element.tagName !== 'A') { + element.setAttribute('tabindex', '0'); + element.setAttribute('role', 'button'); + } + + CoreDom.onActivate(element, async (event) => { + event.preventDefault(); + event.stopPropagation(); + + site = site || CoreSites.getCurrentSite(); + if (!site) { + return; + } + + const confirmMessage = element.getAttribute('data-app-url-confirm'); + const openInApp = element.getAttribute('data-open-in') === 'app'; + + if (confirmMessage) { + try { + await CoreDomUtils.showConfirm(Translate.instant(confirmMessage)); + } catch { + return; + } + } + + if (openInApp) { + site.openInAppWithAutoLoginIfSameSite(url); + } else { + site.openInBrowserWithAutoLoginIfSameSite(url, undefined, { + showBrowserWarning: !confirmMessage, + }); + } + }); + }); + } + /** * Returns the element width in pixels. * diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 37043ffc8..5df73e728 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -48,7 +48,7 @@ export class CoreLinkDirective implements OnInit { @Input() autoLogin = 'check'; @Input() showBrowserWarning = true; // Whether to show a warning before opening browser. Defaults to true. - protected element: Element; + protected element: HTMLElement; constructor( element: ElementRef, @@ -68,22 +68,7 @@ export class CoreLinkDirective implements OnInit { this.element.setAttribute('role', 'button'); } - this.element.addEventListener('click', async (event) => { - this.performAction(event); - }); - - this.element.addEventListener('keydown', (event: KeyboardEvent) => { - if ((event.key == ' ' || event.key == 'Enter')) { - event.preventDefault(); - event.stopPropagation(); - } - }); - - this.element.addEventListener('keyup', (event: KeyboardEvent) => { - if ((event.key == ' ' || event.key == 'Enter')) { - this.performAction(event); - } - }); + CoreDom.onActivate(this.element, (event) => this.performAction(event)); } /** diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index 914cc6ff2..2544650d0 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -481,6 +481,29 @@ export class CoreDom { ); } + /** + * Listen to click and Enter/Space keys in an element. + * + * @param element Element to listen to events. + * @param callback Callback to call when clicked or the key is pressed. + */ + static onActivate(element: HTMLElement, callback: (event: MouseEvent | KeyboardEvent) => void): void { + element.addEventListener('click', (event) => callback(event)); + + element.addEventListener('keydown', (event) => { + if ((event.key == ' ' || event.key == 'Enter')) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + element.addEventListener('keyup', (event) => { + if ((event.key == ' ' || event.key == 'Enter')) { + callback(event); + } + }); + } + } /**