// (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 { AfterViewInit, Directive, ElementRef, OnDestroy, } from '@angular/core'; import { Translate } from '@singletons'; import { CoreIcons } from '@singletons/icons'; import { CoreDom } from '@singletons/dom'; import { CoreWait } from '@singletons/wait'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreModals } from '@services/modals'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreCollapsibleHeaderDirective } from './collapsible-header'; /** * Directive to add the reading mode to the selected html tag. * * Example usage: *
*/ @Directive({ selector: '[core-reading-mode]', }) export class CoreReadingModeDirective implements AfterViewInit, OnDestroy { protected element: HTMLElement; protected viewportPromise?: CoreCancellablePromise; protected disabledStyles: HTMLStyleElement[] = []; protected hiddenElements: HTMLElement[] = []; protected renamedStyles: HTMLElement[] = []; protected enabled = false; protected contentEl?: HTMLIonContentElement; protected header?: CoreCollapsibleHeaderDirective; constructor( element: ElementRef, ) { this.element = element.nativeElement; this.viewportPromise = CoreDom.waitToBeInViewport(this.element); } /** * @inheritdoc */ async ngAfterViewInit(): Promise { await this.viewportPromise; await CoreWait.nextTick(); this.addTextViewerButton(); } /** * Add text viewer button to enable the reading mode. */ protected async addTextViewerButton(): Promise { const page = CoreDom.closest(this.element, '.ion-page'); this.contentEl = page?.querySelector('ion-content') ?? undefined; const toolbar = page?.querySelector('ion-header ion-toolbar ion-buttons[slot="end"]'); if (!toolbar || toolbar.querySelector('.core-text-viewer-button')) { return; } this.contentEl?.classList.add('core-reading-mode-content'); const header = CoreDirectivesRegistry.resolve(page?.querySelector('ion-header'), CoreCollapsibleHeaderDirective); if (header) { this.header = header; } const label = Translate.instant('core.viewer.enterreadingmode'); const button = document.createElement('ion-button'); button.classList.add('core-text-viewer-button'); button.setAttribute('aria-label', label); button.setAttribute('fill', 'clear'); const iconName = 'book-open-reader'; const src = CoreIcons.getIconSrc('font-awesome', 'solid', iconName); // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. button.innerHTML = ``; toolbar.appendChild(button); button.addEventListener('click', (e: Event) => { if (!this.element.innerHTML) { return; } e.preventDefault(); e.stopPropagation(); if (!this.enabled) { this.enterReadingMode(); } else { this.showReadingSettings(); } }); } /** * Enters the reading mode. */ protected async enterReadingMode(): Promise { this.enabled = true; CoreViewer.loadReadingModeSettings(); this.header?.setEnabled(false); document.body.classList.add('core-reading-mode-enabled'); // Disable all styles in element. this.disabledStyles = Array.from(this.element.querySelectorAll('style:not(disabled)')); this.disabledStyles.forEach((style) => { style.disabled = true; }); // Rename style attributes on DOM elements. this.renamedStyles = Array.from(this.element.querySelectorAll('*[style]')); this.renamedStyles.forEach((element: HTMLElement) => { this.renamedStyles.push(element); element.setAttribute('data-original-style', element.getAttribute('style') || ''); element.removeAttribute('style'); }); // Navigate to parent hidding all other elements. let currentChild = this.element; let parent = currentChild.parentElement; while (parent && parent.tagName.toLowerCase() !== 'ion-content') { Array.from(parent.children).forEach((child: HTMLElement) => { if (child !== currentChild && child.tagName.toLowerCase() !== 'swiper-slide') { this.hiddenElements.push(child); child.classList.add('hide-on-reading-mode'); } }); currentChild = parent; parent = currentChild.parentElement; } } /** * Disable the reading mode. */ protected async disableReadingMode(): Promise { this.enabled = false; document.body.classList.remove('core-reading-mode-enabled'); this.header?.setEnabled(true); // Enable all styles in element. this.disabledStyles.forEach((style) => { style.disabled = false; }); this.disabledStyles = []; // Rename style attributes on DOM elements. this.renamedStyles.forEach((element) => { element.setAttribute('style', element.getAttribute('data-original-style') || ''); element.removeAttribute('data-original-style'); }); this.renamedStyles = []; this.hiddenElements.forEach((element) => { element.classList.remove('hide-on-reading-mode'); }); this.hiddenElements = []; } /** * Show the reading settings. */ protected async showReadingSettings(): Promise { const { CoreReadingModeSettingsModalComponent } = await import('@features/viewer/components/reading-mode-settings/reading-mode-settings'); const exit = await CoreModals.openModal({ component: CoreReadingModeSettingsModalComponent, initialBreakpoint: 0.5, breakpoints: [0, 1], cssClass: 'core-modal-auto-height', }); if (exit) { this.disableReadingMode(); } } /** * @inheritdoc */ ngOnDestroy(): void { this.disableReadingMode(); this.viewportPromise?.cancel(); } }