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 = {}; #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 = 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 = 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/'); } }