import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component'; import { HELP_DOCS, MANUAL_BASE_URL } from './tokens'; const VIRTUAL_SCROLL_ID = 'virtual-scroll'; const ANNOTATIONS_LIST_ID = 'annotations-list'; const SCROLL_BUTTONS_IDS = ['scroll-up', 'scroll-down']; export const ScrollableParentViews = { VIRTUAL_SCROLL: 'VIRTUAL_SCROLL', ANNOTATIONS_LIST: 'ANNOTATIONS_LIST', } as const; export type ScrollableParentView = keyof typeof ScrollableParentViews; interface Helper { readonly element: HTMLElement; readonly helperElement: HTMLElement; readonly scrollableParentView: ScrollableParentView; readonly dialogElement: boolean; } @Injectable({ providedIn: 'root', }) export class HelpModeService { private readonly _isHelpModeActive$ = new BehaviorSubject(false); readonly isHelpModeActive$ = this._isHelpModeActive$.asObservable(); private readonly _helpModeDialogIsOpened$ = new BehaviorSubject(false); readonly helpModeDialogIsOpened$ = this._helpModeDialogIsOpened$.asObservable(); private readonly _helperElements: Record = {}; private readonly _renderer: Renderer2; private _dialogMode = false; constructor( @Inject(HELP_DOCS) private readonly _docs: Record>, @Inject(MANUAL_BASE_URL) private readonly _manualBaseURL: string, private readonly _dialog: MatDialog, private readonly _rendererFactory: RendererFactory2, private readonly _translateService: TranslateService, ) { this._renderer = this._rendererFactory.createRenderer(null, null); } get isHelpModeActive(): boolean { return this._isHelpModeActive$.getValue(); } get helpModeDialogIsOpened(): boolean { return this._helpModeDialogIsOpened$.getValue(); } openHelpModeDialog(): MatDialogRef { this._helpModeDialogIsOpened$.next(true); const ref = this._dialog.open(HelpModeDialogComponent, { width: '600px', }); firstValueFrom(ref.afterClosed()).then(() => { this._helpModeDialogIsOpened$.next(false); }); return ref; } getDocsLink(elementName: string): string { return this._docs[elementName] ? `${this._manualBaseURL}${this._docs[elementName][this._translateService.currentLang]}` : ''; } activateHelpMode(dialogMode: boolean = false): void { if (!this.isHelpModeActive) { document.body.style.setProperty('overflow', 'hidden'); this._isHelpModeActive$.next(true); this.openHelpModeDialog(); this._dialogMode = dialogMode; setTimeout(() => { this._enableHelperElements(); }); } } deactivateHelpMode(): void { if (this.isHelpModeActive) { document.body.style.removeProperty('overflow'); this._isHelpModeActive$.next(false); this._disableHelperElements(); } } highlightHelperElements(): void { Object.values(this._helperElements).forEach(({ element, helperElement }) => { this._renderer.addClass(helperElement, 'help-highlight'); setTimeout(() => { this._renderer.removeClass(helperElement, 'help-highlight'); }, 500); }); } addElement( helperElementName: string, element: HTMLElement, helperElement: HTMLElement, scrollableParentView: ScrollableParentView, dialogElement: boolean, ): void { this._helperElements[helperElementName] = { element, helperElement, scrollableParentView, dialogElement }; } updateHelperElements() { Object.values(this._helperElements).forEach(({ element, helperElement, scrollableParentView }) => { this._updateHelperElement(element, helperElement, scrollableParentView); }); } private _isElementVisible(element: HTMLElement, scrollableParentView: ScrollableParentView): boolean { let elementRect: DOMRect = element.getBoundingClientRect(); if (elementRect.top === 0 && elementRect.left === 0 && elementRect.bottom === 0 && elementRect.bottom === 0) { return false; } if (scrollableParentView) { const scrollableElementId = scrollableParentView === ScrollableParentViews.VIRTUAL_SCROLL ? VIRTUAL_SCROLL_ID : ANNOTATIONS_LIST_ID; 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 (scrollableParentView === ScrollableParentViews.VIRTUAL_SCROLL) { for (const id of SCROLL_BUTTONS_IDS) { const scroll: HTMLElement = document.getElementById(id); elementRect = 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; } } } } return true; } private _updateHelperElement(element: HTMLElement, helperElement: HTMLElement, scrollableParentView: ScrollableParentView) { if (this._isElementVisible(element, scrollableParentView)) { const dimensions = this._getElementDimensions(element); helperElement.style.cssText = ` top:${dimensions.y}px; left:${dimensions.x}px; width:${dimensions.width}px; height:${dimensions.height}px; `; helperElement.classList.add('help-mode'); } else { helperElement.classList.remove('help-mode'); } } private _enableHelperElements() { Object.values(this._helperElements).forEach(({ element, helperElement, scrollableParentView, dialogElement }) => { if (this._dialogMode === dialogElement ) { document.body.appendChild(helperElement); this._updateHelperElement(element, helperElement, scrollableParentView); } }); } private _disableHelperElements() { Object.values(this._helperElements).forEach(({ helperElement }) => { if (document.body.contains(helperElement)) { document.body.removeChild(helperElement); } }); } private _getElementDimensions(element: HTMLElement) { const rect = element.getBoundingClientRect(); return { y: rect.top, x: rect.left, height: rect.height, width: rect.width, }; } }