common-ui/src/lib/help-mode/help-mode.service.ts

291 lines
11 KiB
TypeScript

import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { getConfig } from '../services';
import { IqserUserPreferenceService } from '../services';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HELP_MODE_KEYS, MANUAL_BASE_URL } from './tokens';
import { HelpModeKey } from './types';
import {
DOCUMINE_THEME_CLASS,
HELP_HIGHLIGHT_CLASS,
HELP_MODE_CLASS,
OVERLAPPING_DROPDOWNS_IDS,
OverlappingElement,
PDF_TRON_IFRAME_ID,
SCROLL_BUTTONS_IDS,
SCROLLABLE_PARENT_VIEWS_IDS,
ScrollableParentView,
ScrollableParentViews,
WEB_VIEWER_ELEMENTS,
} from './utils/constants';
import { toSignal } from '@angular/core/rxjs-interop';
export interface Helper {
readonly element: HTMLElement;
readonly helperElement: HTMLElement;
readonly scrollableParentView?: ScrollableParentView;
readonly overlappingElements?: OverlappingElement[];
readonly dialogElement?: boolean;
readonly iframeElement?: boolean;
}
@Injectable()
export class HelpModeService {
readonly #isHelpModeActive$ = new BehaviorSubject(false);
readonly #helpModeDialogIsOpened$ = new BehaviorSubject(false);
readonly #renderer: Renderer2;
readonly #isDocumine = getConfig().IS_DOCUMINE;
#helpers: Record<string, Helper> = {};
#dialogMode = false;
readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
readonly isHelpModeActive = toSignal(this.isHelpModeActive$, { initialValue: false });
readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
constructor(
@Inject(HELP_MODE_KEYS) private readonly _keys: HelpModeKey[],
@Inject(MANUAL_BASE_URL) private readonly _manualBaseURL: string,
private readonly _dialog: MatDialog,
private readonly _rendererFactory: RendererFactory2,
private readonly _translateService: TranslateService,
private readonly _iqserUserPreferenceService: IqserUserPreferenceService,
) {
this.#renderer = this._rendererFactory.createRenderer(null, null);
}
get helpModeDialogIsOpened(): boolean {
return this.#helpModeDialogIsOpened$.getValue();
}
async openHelpModeDialog() {
if (!this._iqserUserPreferenceService.getHelpModeDialog()) {
this.#helpModeDialogIsOpened$.next(true);
const ref = this._dialog.open(HelpModeDialogComponent, {
width: '600px',
});
firstValueFrom(ref.afterClosed()).then(result => {
this.#helpModeDialogIsOpened$.next(false);
if (result) {
this._iqserUserPreferenceService.toggleHelpModeDialog();
}
});
}
}
activateHelpMode(dialogMode: boolean = false): void {
if (!this.isHelpModeActive()) {
document.body.style.setProperty('overflow', 'unset');
this.#isHelpModeActive$.next(true);
this.openHelpModeDialog();
this.#dialogMode = dialogMode;
setTimeout(() => {
this.#createHelpers();
this.#createWebViewerHelpers();
this.#enableHelperElements();
}, 500);
}
}
deactivateHelpMode(): void {
if (this.isHelpModeActive()) {
document.body.style.removeProperty('overflow');
this.#isHelpModeActive$.next(false);
this.#disableHelperElements();
this.#helpers = {};
}
}
highlightHelperElements(): void {
Object.values(this.#helpers).forEach(helper => {
this.#renderer.addClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
setTimeout(() => {
this.#renderer.removeClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
}, 500);
});
}
updateHelperElements() {
Object.values(this.#helpers).forEach(helper => this.#updateHelperElement(helper));
}
#createHelpers() {
for (const key of Object.values(this._keys)) {
const elements = document.querySelectorAll(`[help-mode-key='${key.elementKey}']`);
elements.forEach(element => {
const helperKey = `${key.elementKey}-${this.#generateId()}`;
this.#helpers[helperKey] = {
element: element as HTMLElement,
helperElement: this.#getHelperElement(element as HTMLElement, key.documentKey),
scrollableParentView: key.scrollableParentView,
overlappingElements: key.overlappingElements,
dialogElement: key.elementKey.endsWith('_DIALOG'),
};
});
}
}
#createWebViewerHelpers(): void {
const iframe: HTMLIFrameElement = document.getElementById(PDF_TRON_IFRAME_ID) as HTMLIFrameElement;
if (iframe) {
WEB_VIEWER_ELEMENTS.forEach(e => {
const element: HTMLElement = iframe.contentWindow?.document.querySelector(e.querySelector) as HTMLElement;
this.#helpers[e.documentKey] = {
element,
helperElement: this.#getHelperElement(element, e.documentKey),
dialogElement: false,
iframeElement: true,
};
});
}
}
#getHelperElement(element: HTMLElement, key: string): HTMLElement {
const helperElement = this.#renderer.createElement('a') as HTMLElement;
this.#renderer.setAttribute(helperElement, 'href', this.#generateDocsLink(key));
this.#renderer.setAttribute(helperElement, 'target', '_blank');
this.#renderer.addClass(helperElement, HELP_MODE_CLASS);
if (this.#isDocumine) {
this.#renderer.addClass(helperElement, DOCUMINE_THEME_CLASS);
}
return helperElement;
}
#generateId(): string {
return Math.random().toString(36).substring(2, 9);
}
#generateDocsLink(key: string) {
const currentLang = this._translateService.currentLang;
return `${this._manualBaseURL}/${currentLang}/index-${currentLang}.html?contextId=${key}`;
}
#isElementVisible(helper: Helper): boolean {
if (helper.iframeElement && !this.#isFilePreviewPage()) {
return false;
}
let elementRect: DOMRect = helper.element.getBoundingClientRect();
if (elementRect.top === 0 && elementRect.left === 0 && elementRect.bottom === 0 && elementRect.bottom === 0) {
return false;
}
if (helper.scrollableParentView) {
const scrollableElementId = SCROLLABLE_PARENT_VIEWS_IDS[helper.scrollableParentView];
const scrollableElement: HTMLElement = <HTMLElement>document.getElementById(scrollableElementId);
if (!scrollableElement) {
return false;
}
const scrollableElementRect = scrollableElement.getBoundingClientRect();
if (
!(
elementRect.top > scrollableElementRect.top &&
elementRect.left > scrollableElementRect.left &&
elementRect.bottom < scrollableElementRect.bottom &&
elementRect.right < scrollableElementRect.right
)
) {
return false;
}
if (helper.scrollableParentView === ScrollableParentViews.VIRTUAL_SCROLL) {
for (const id of SCROLL_BUTTONS_IDS) {
const scroll: HTMLElement = <HTMLElement>document.getElementById(id);
elementRect = helper.element.getBoundingClientRect();
const scrollRect = scroll?.getBoundingClientRect();
if (
elementRect.top + elementRect.height > scrollRect.top &&
elementRect.left + elementRect.width > scrollRect.left &&
elementRect.bottom - elementRect.height < scrollRect.bottom &&
elementRect.right - elementRect.width < scrollRect.right
) {
return false;
}
}
}
}
if (helper.overlappingElements) {
for (const overlappingElementName of helper.overlappingElements) {
const overlappingElement = document.getElementById(OVERLAPPING_DROPDOWNS_IDS[overlappingElementName]);
if (!overlappingElement) {
continue;
}
const overlappingElementRect = overlappingElement.getBoundingClientRect();
return !(
Math.max(elementRect.left, overlappingElementRect.left) < Math.min(elementRect.right, overlappingElementRect.right) &&
Math.max(elementRect.top, overlappingElementRect.top) < Math.min(elementRect.bottom, overlappingElementRect.bottom)
);
}
}
return true;
}
#updateHelperElement(helper: Helper) {
if (this.#isElementVisible(helper)) {
const dimensions = this.#getElementDimensions(helper.element);
if (helper.iframeElement) {
const iframe: HTMLIFrameElement = document.getElementById(PDF_TRON_IFRAME_ID) as HTMLIFrameElement;
const iframeRect = iframe.getBoundingClientRect();
dimensions.y += iframeRect.top;
dimensions.x += iframeRect.left;
}
helper.helperElement.style.cssText = `
top:${dimensions.y}px;
left:${dimensions.x}px;
width:${dimensions.width}px;
height:${dimensions.height}px;
`;
helper.helperElement.classList.add(HELP_MODE_CLASS);
} else {
helper.helperElement.classList.remove(HELP_MODE_CLASS);
}
}
#enableHelperElements() {
Object.values(this.#helpers).forEach(helper => {
if (this.#dialogMode === helper.dialogElement) {
document.body.appendChild(helper.helperElement);
this.#updateHelperElement(helper);
}
});
}
#disableHelperElements() {
Object.values(this.#helpers).forEach(helper => {
if (document.body.contains(helper.helperElement)) {
document.body.removeChild(helper.helperElement);
}
});
}
#getElementDimensions(element: HTMLElement) {
const rect = element.getBoundingClientRect();
return {
y: rect.top,
x: rect.left,
height: rect.height,
width: rect.width,
};
}
#isFilePreviewPage() {
return window.location.href.includes('/file/');
}
}