Vmeda.Online/src/core/directives/reading-mode.ts

215 lines
7.0 KiB
TypeScript

// (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:
* <div core-reading-mode>
*/
@Directive({
selector: '[core-reading-mode]',
})
export class CoreReadingModeDirective implements AfterViewInit, OnDestroy {
protected element: HTMLElement;
protected viewportPromise?: CoreCancellablePromise<void>;
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<void> {
await this.viewportPromise;
await CoreWait.nextTick();
this.addTextViewerButton();
}
/**
* Add text viewer button to enable the reading mode.
*/
protected async addTextViewerButton(): Promise<void> {
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 = `<ion-icon name="fas-${iconName}" aria-hidden="true" src="${src}"></ion-icon>`;
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<void> {
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<void> {
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<void> {
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();
}
}