From 6f267657b7ba4a7cf5715c5c4d6b1f531c37a765 Mon Sep 17 00:00:00 2001 From: Mostafa Abdo Date: Tue, 18 Feb 2025 18:11:48 -0800 Subject: [PATCH] feature: add tooltip module --- CSETWebNg/package-lock.json | 14 - CSETWebNg/package.json | 3 +- CSETWebNg/src/app/app.module.ts | 2 +- .../src/app/tooltip/options.interface.ts | 37 ++ CSETWebNg/src/app/tooltip/options.service.ts | 8 + CSETWebNg/src/app/tooltip/options.ts | 38 ++ .../src/app/tooltip/tooltip.component.html | 10 + .../src/app/tooltip/tooltip.component.sass | 126 +++++ .../src/app/tooltip/tooltip.component.ts | 215 ++++++++ .../src/app/tooltip/tooltip.directive.ts | 479 ++++++++++++++++++ CSETWebNg/src/app/tooltip/tooltip.module.ts | 33 ++ 11 files changed, 948 insertions(+), 17 deletions(-) create mode 100644 CSETWebNg/src/app/tooltip/options.interface.ts create mode 100644 CSETWebNg/src/app/tooltip/options.service.ts create mode 100644 CSETWebNg/src/app/tooltip/options.ts create mode 100644 CSETWebNg/src/app/tooltip/tooltip.component.html create mode 100644 CSETWebNg/src/app/tooltip/tooltip.component.sass create mode 100644 CSETWebNg/src/app/tooltip/tooltip.component.ts create mode 100644 CSETWebNg/src/app/tooltip/tooltip.directive.ts create mode 100644 CSETWebNg/src/app/tooltip/tooltip.module.ts diff --git a/CSETWebNg/package-lock.json b/CSETWebNg/package-lock.json index 799e47848..f75de29d5 100644 --- a/CSETWebNg/package-lock.json +++ b/CSETWebNg/package-lock.json @@ -26,7 +26,6 @@ "@angular/platform-browser-dynamic": "^18.2.11", "@angular/platform-server": "^18.2.11", "@angular/router": "^18.2.11", - "@cloudfactorydk/ng2-tooltip-directive": "^18.0.0", "@fortawesome/angular-fontawesome": "^0.15.0", "@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0", @@ -3096,19 +3095,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cloudfactorydk/ng2-tooltip-directive": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@cloudfactorydk/ng2-tooltip-directive/-/ng2-tooltip-directive-18.0.0.tgz", - "integrity": "sha512-fFEC73DIwxSA980XT3eSyoTar2kjShiDan+iqxo9UeMrgaPh8anuS+bIgTwh++U9+qQUkzZkDfyIUjmMGUb7rw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "peerDependencies": { - "@angular/common": "^18.0.0", - "@angular/core": "^18.0.0" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/CSETWebNg/package.json b/CSETWebNg/package.json index f1ec51db3..2b84bc597 100644 --- a/CSETWebNg/package.json +++ b/CSETWebNg/package.json @@ -33,7 +33,6 @@ "@angular/platform-browser-dynamic": "^18.2.11", "@angular/platform-server": "^18.2.11", "@angular/router": "^18.2.11", - "@cloudfactorydk/ng2-tooltip-directive": "^18.0.0", "@fortawesome/angular-fontawesome": "^0.15.0", "@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0", @@ -66,8 +65,8 @@ "ngx-ellipsis": "^5.0.1", "pdfmake": "^0.2.18", "rxjs": "^7.8.1", - "sass": "^1.83.4", "sanitize-html": "^2.14.0", + "sass": "^1.83.4", "screenfull": "^5.2.0", "style-loader": "^4.0.0", "swiper": "^8.4.7", diff --git a/CSETWebNg/src/app/app.module.ts b/CSETWebNg/src/app/app.module.ts index 74af5f83d..6b05f5d97 100644 --- a/CSETWebNg/src/app/app.module.ts +++ b/CSETWebNg/src/app/app.module.ts @@ -337,7 +337,7 @@ import { CisCommentsmarkedComponent } from './reports/cis-commentsmarked/cis-com import { MaturityQuestionsAcetComponent } from './assessment/questions/maturity-questions/maturity-questions-acet.component'; import { MaturityQuestionsIseComponent } from './assessment/questions/maturity-questions/maturity-questions-ise.component'; import { EdmComponent } from './reports/edm/edm.component'; -import { TooltipModule } from '@cloudfactorydk/ng2-tooltip-directive'; +import { TooltipModule } from './tooltip/tooltip.module'; import { QuestionTextComponent } from './assessment/questions/question-text/question-text.component'; import { QuestionTextCpgComponent } from './assessment/questions/question-text/question-text-cpg/question-text-cpg.component'; import { AcetFilteringService } from './services/filtering/maturity-filtering/acet-filtering.service'; diff --git a/CSETWebNg/src/app/tooltip/options.interface.ts b/CSETWebNg/src/app/tooltip/options.interface.ts new file mode 100644 index 000000000..65eea7b2d --- /dev/null +++ b/CSETWebNg/src/app/tooltip/options.interface.ts @@ -0,0 +1,37 @@ +export interface TooltipOptions { + 'placement'?: string; + 'autoPlacement'?: boolean; + 'content-type'?: 'string' | 'html' | 'template'; + 'contentType'?: 'string' | 'html' | 'template'; + 'delay'?: number; + 'show-delay'?: number; + 'showDelay'?: number; + 'hide-delay'?: number; + 'hideDelay'?: number; + 'hide-delay-mobile'?: number; + 'hideDelayMobile'?: number; + 'hideDelayTouchscreen'?: number; + 'z-index'?: number; + 'zIndex'?: number; + 'animation-duration'?: number; + 'animationDuration'?: number; + 'animation-duration-default'?: number; + 'animationDurationDefault'?: number; + 'trigger'?: string; + 'tooltip-class'?: string; + 'tooltipClass'?: string; + 'display'?: boolean; + 'display-mobile'?: boolean; + 'displayMobile'?: boolean; + 'displayTouchscreen'?: boolean; + 'shadow'?: boolean; + 'theme'?: "dark" | "light"; + 'offset'?: number; + 'width'?: string; + 'max-width'?: string; + 'maxWidth'?: string; + 'id'?: string | number; + 'hideDelayAfterClick'?: number; + 'pointerEvents'?: 'auto' | 'none'; + 'position'?: {top: number, left: number}; +} \ No newline at end of file diff --git a/CSETWebNg/src/app/tooltip/options.service.ts b/CSETWebNg/src/app/tooltip/options.service.ts new file mode 100644 index 000000000..2bc7a1b0e --- /dev/null +++ b/CSETWebNg/src/app/tooltip/options.service.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; +import { TooltipOptions } from './options.interface'; + +/** + * This is not a real service, but it looks like it from the outside. + * It's just an InjectionToken used to import the config (initOptions) object, provided from the outside + */ +export const TooltipOptionsService = new InjectionToken('TooltipOptions'); diff --git a/CSETWebNg/src/app/tooltip/options.ts b/CSETWebNg/src/app/tooltip/options.ts new file mode 100644 index 000000000..bee9f6e59 --- /dev/null +++ b/CSETWebNg/src/app/tooltip/options.ts @@ -0,0 +1,38 @@ +export const defaultOptions = { + 'placement': 'top', + 'autoPlacement': true, + 'contentType': 'string', + 'showDelay': 0, + 'hideDelay': 300, + 'hideDelayMobile': 0, + 'hideDelayTouchscreen': 0, + 'zIndex': 0, + 'animationDuration': 300, + 'animationDurationDefault': 300, + 'trigger': 'hover', + 'tooltipClass': '', + 'display': true, + 'displayMobile': true, + 'displayTouchscreen': true, + 'shadow': true, + 'theme': 'dark', + 'offset': 8, + 'maxWidth': '', + 'id': false, + 'hideDelayAfterClick': 2000 +} + +export const backwardCompatibilityOptions:any = { + 'delay': 'showDelay', + 'show-delay': 'showDelay', + 'hide-delay': 'hideDelay', + 'hide-delay-mobile': 'hideDelayTouchscreen', + 'hideDelayMobile': 'hideDelayTouchscreen', + 'z-index': 'zIndex', + 'animation-duration': 'animationDuration', + 'animation-duration-default': 'animationDurationDefault', + 'tooltip-class': 'tooltipClass', + 'display-mobile': 'displayTouchscreen', + 'displayMobile': 'displayTouchscreen', + 'max-width': 'maxWidth' +} \ No newline at end of file diff --git a/CSETWebNg/src/app/tooltip/tooltip.component.html b/CSETWebNg/src/app/tooltip/tooltip.component.html new file mode 100644 index 000000000..4574c5b0d --- /dev/null +++ b/CSETWebNg/src/app/tooltip/tooltip.component.html @@ -0,0 +1,10 @@ +
+ +
+ + +
+ + +
+
diff --git a/CSETWebNg/src/app/tooltip/tooltip.component.sass b/CSETWebNg/src/app/tooltip/tooltip.component.sass new file mode 100644 index 000000000..4ccbc1595 --- /dev/null +++ b/CSETWebNg/src/app/tooltip/tooltip.component.sass @@ -0,0 +1,126 @@ +\:host + max-width: 200px + background-color: black + color: #fff + text-align: center + border-radius: 6px + padding: 5px 8px + position: absolute + pointer-events: none + z-index: 1000 + display: block + opacity: 0 + -webkit-transition: opacity 300ms + -moz-transition: opacity 300ms + -o-transition: opacity 300ms + transition: opacity 300ms + top: 0 + left: 0 + +\:host.tooltip-show + opacity: 1 + +\:host.tooltip-shadow + box-shadow: 0 7px 15px -5px rgba(0, 0, 0, 0.4) +\:host.tooltip-light.tooltip-shadow + box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.4) + +\:host.tooltip::after + content: "" + position: absolute + border-style: solid + +\:host.tooltip-top::after + top: 100% + left: 50% + margin-left: -5px + border-width: 5px + border-color: black transparent transparent transparent + +\:host.tooltip-bottom::after + bottom: 100% + left: 50% + margin-left: -5px + border-width: 5px + border-color: transparent transparent black transparent + +\:host.tooltip-left::after + top: 50% + left: 100% + margin-top: -5px + border-width: 5px + border-color: transparent transparent transparent black + +\:host.tooltip-right::after + top: 50% + right: 100% + margin-top: -5px + border-width: 5px + border-color: transparent black transparent transparent + +// Light +\:host.tooltip-light::after + display: none + +\:host.tooltip-light + border: 1px solid rgba(0,0,0,.06) + background-color: #fff + color: black + .tooltip-arrow + position: absolute + width: 10px + height: 10px + transform: rotate(135deg) + background-color: rgba(0,0,0,.07) + .tooltip-arrow::after + background-color: #fff + content: '' + display: block + position: absolute + width: 10px + height: 10px + +\:host.tooltip-top.tooltip-light + margin-top: -2px + .tooltip-arrow + top: 100% + left: 50% + margin-top: -4px + margin-left: -5px + background: linear-gradient(to bottom left,rgba(0,0,0,.07) 50%,transparent 50%) + .tooltip-arrow::after + top: 1px + right: 1px + +\:host.tooltip-bottom.tooltip-light + .tooltip-arrow + bottom: 100% + left: 50% + margin-bottom: -4px + margin-left: -5px + background: linear-gradient(to top right,rgba(0,0,0,.1) 50%,transparent 50%) + .tooltip-arrow::after + top: -1px + right: -1px + +\:host.tooltip-left.tooltip-light + .tooltip-arrow + top: 50% + left: 100% + margin-top: -5px + margin-left: -4px + background: linear-gradient(to bottom right,rgba(0,0,0,.07) 50%,transparent 50%) + .tooltip-arrow::after + top: 1px + right: -1px + +\:host.tooltip-right.tooltip-light + .tooltip-arrow + top: 50% + right: 100% + margin-top: -5px + margin-right: -4px + background: linear-gradient(to top left,rgba(0,0,0,.07) 50%,transparent 50%) + .tooltip-arrow::after + top: -1px + right: 1px diff --git a/CSETWebNg/src/app/tooltip/tooltip.component.ts b/CSETWebNg/src/app/tooltip/tooltip.component.ts new file mode 100644 index 000000000..eb2ab0806 --- /dev/null +++ b/CSETWebNg/src/app/tooltip/tooltip.component.ts @@ -0,0 +1,215 @@ +import {Component, ElementRef, HostListener, HostBinding, Input, OnInit, EventEmitter, Renderer2} from '@angular/core'; + +@Component({ + selector: 'tooltip', + templateUrl: './tooltip.component.html', + host: { + 'class': 'tooltip' + }, + styleUrls: ['./tooltip.component.sass'] +}) + +export class TooltipComponent { + _show: boolean = false; + events = new EventEmitter(); + + @Input() data: any; + + @HostBinding('style.top') hostStyleTop!: string; + @HostBinding('style.left') hostStyleLeft!: string; + @HostBinding('style.z-index') hostStyleZIndex!: number; + @HostBinding('style.transition') hostStyleTransition!: string; + @HostBinding('style.width') hostStyleWidth!: string; + @HostBinding('style.max-width') hostStyleMaxWidth!: string; + @HostBinding('style.pointer-events') hostStylePointerEvents!: string; + @HostBinding('class.tooltip-show') hostClassShow!: boolean; + @HostBinding('class.tooltip-shadow') hostClassShadow!: boolean; + @HostBinding('class.tooltip-light') hostClassLight!: boolean; + + @HostListener('transitionend', ['$event']) + transitionEnd(event:any) { + if (this.show) { + this.events.emit({ + type: 'shown' + }); + } + } + + @Input() set show(value: boolean) { + if (value) { + this.setPosition(); + } + this._show = this.hostClassShow = value; + } + get show(): boolean { + return this._show; + } + + get placement() { + return this.data.options.placement; + } + + get autoPlacement() { + return this.data.options.autoPlacement; + } + + get element() { + return this.data.element; + } + + get elementPosition() { + return this.data.elementPosition; + } + + get options() { + return this.data.options; + } + + get value() { + return this.data.value; + } + + get tooltipOffset(): number { + return Number(this.data.options.offset); + } + + get isThemeLight() { + return this.options['theme'] === 'light'; + } + + constructor(private elementRef: ElementRef, private renderer: Renderer2) {} + + ngOnInit() { + this.setCustomClass(); + this.setStyles(); + } + + setPosition(): void { + if (this.setHostStyle(this.placement)) { + this.setPlacementClass(this.placement); + return; + } else { + /* Is tooltip outside the visible area */ + const placements = ['top', 'right', 'bottom', 'left']; + let isPlacementSet; + + for (const placement of placements) { + if (this.setHostStyle(placement)) { + this.setPlacementClass(placement); + isPlacementSet = true; + return; + } + } + + /* Set original placement */ + if (!isPlacementSet) { + this.setHostStyle(this.placement, true); + this.setPlacementClass(this.placement); + } + } + } + + + setPlacementClass(placement: string): void { + this.renderer.addClass(this.elementRef.nativeElement, 'tooltip-' + placement); + } + + setHostStyle(placement: string, disableAutoPlacement: boolean = false): boolean { + const isSvg = this.element instanceof SVGElement; + const tooltip = this.elementRef.nativeElement; + const isCustomPosition = !this.elementPosition.right; + + let elementHeight = isSvg ? this.element.getBoundingClientRect().height : this.element.offsetHeight; + let elementWidth = isSvg ? this.element.getBoundingClientRect().width : this.element.offsetWidth; + const tooltipHeight = tooltip.clientHeight; + const tooltipWidth = tooltip.clientWidth; + const scrollY = window.pageYOffset; + + if (isCustomPosition) { + elementHeight = 0; + elementWidth = 0; + } + + let topStyle; + let leftStyle; + + if (placement === 'top') { + topStyle = (this.elementPosition.top + scrollY) - (tooltipHeight + this.tooltipOffset); + } + + if (placement === 'bottom') { + topStyle = (this.elementPosition.top + scrollY) + elementHeight + this.tooltipOffset; + } + + if (placement === 'top' || placement === 'bottom') { + leftStyle = (this.elementPosition.left + elementWidth / 2) - tooltipWidth / 2; + } + + if (placement === 'left') { + leftStyle = this.elementPosition.left - tooltipWidth - this.tooltipOffset; + } + + if (placement === 'right') { + leftStyle = this.elementPosition.left + elementWidth + this.tooltipOffset; + } + + if (placement === 'left' || placement === 'right') { + topStyle = (this.elementPosition.top + scrollY) + elementHeight / 2 - tooltip.clientHeight / 2; + } + + /* Is tooltip outside the visible area */ + if (this.autoPlacement && !disableAutoPlacement) { + const topEdge = topStyle; + const bottomEdge = topStyle + tooltipHeight; + const leftEdge = leftStyle; + const rightEdge = leftStyle + tooltipWidth; + const bodyHeight = window.innerHeight + scrollY; + const bodyWidth = document.body.clientWidth; + + if (topEdge < 0 || bottomEdge > bodyHeight || leftEdge < 0 || rightEdge > bodyWidth) { + return false; + } + } + + this.hostStyleTop = topStyle + 'px'; + this.hostStyleLeft = leftStyle + 'px'; + return true; + } + + setZIndex(): void { + if (this.options['zIndex'] !== 0) { + this.hostStyleZIndex = this.options['zIndex']; + } + } + + setPointerEvents(): void { + if (this.options['pointerEvents']) { + this.hostStylePointerEvents = this.options['pointerEvents']; + } + } + + setCustomClass(){ + if (this.options['tooltipClass']) { + this.options['tooltipClass'].split(' ').forEach((className:any) => { + this.renderer.addClass(this.elementRef.nativeElement, className); + }); + } + } + + setAnimationDuration() { + if (Number(this.options['animationDuration']) != this.options['animationDurationDefault']) { + this.hostStyleTransition = 'opacity ' + this.options['animationDuration'] + 'ms'; + } + } + + setStyles() { + this.setZIndex(); + this.setPointerEvents(); + this.setAnimationDuration(); + + this.hostClassShadow = this.options['shadow']; + this.hostClassLight = this.isThemeLight; + this.hostStyleMaxWidth = this.options['maxWidth']; + this.hostStyleWidth = this.options['width'] ? this.options['width'] : ''; + } +} diff --git a/CSETWebNg/src/app/tooltip/tooltip.directive.ts b/CSETWebNg/src/app/tooltip/tooltip.directive.ts new file mode 100644 index 000000000..6873c8c3c --- /dev/null +++ b/CSETWebNg/src/app/tooltip/tooltip.directive.ts @@ -0,0 +1,479 @@ +import { Directive, ElementRef, HostListener, Input, ComponentFactoryResolver, EmbeddedViewRef, ApplicationRef, Injector, ComponentRef, OnInit, Output, EventEmitter, OnDestroy, Inject, Optional, SimpleChanges } from '@angular/core'; +import { TooltipComponent } from './tooltip.component'; +import { TooltipOptionsService } from './options.service'; +import { defaultOptions, backwardCompatibilityOptions } from './options'; +import { TooltipOptions } from './options.interface'; + +export interface AdComponent { + data: any; + show: boolean; + close: boolean; + events: any; +} + +@Directive({ + selector: '[tooltip]', + exportAs: 'tooltip', +}) + +export class TooltipDirective { + + hideTimeoutId!: number; + destroyTimeoutId!: number; + hideAfterClickTimeoutId!: number; + createTimeoutId!: number; + showTimeoutId!: number; + componentRef: any; + elementPosition: any; + _id: any; + _options: any = {}; + _defaultOptions: any; + _destroyDelay!: number; + componentSubscribe: any; + _contentType: "string" | "html" | "template" = "string"; + _showDelay!: number; + _hideDelay!: number; + _zIndex!: number; + _tooltipClass!: string; + _animationDuration!: number; + _maxWidth!: string; + + @Input('options') set options(value: TooltipOptions) { + if (value && defaultOptions) { + this._options = value; + } + } + get options() { + return this._options; + } + + @Input('tooltip') tooltipValue!: string; + @Input('placement') placement!: string; + @Input('autoPlacement') autoPlacement!: boolean; + + // Content type + @Input('content-type') set contentTypeBackwardCompatibility(value: "string" | "html" | "template") { + if (value) { + this._contentType = value; + } + } + @Input('contentType') set contentType(value: "string" | "html" | "template") { + if (value) { + this._contentType = value; + } + } + get contentType() { + return this._contentType; + } + + @Input('hide-delay-mobile') hideDelayMobile!: number; + @Input('hideDelayTouchscreen') hideDelayTouchscreen!: number; + + // z-index + @Input('z-index') set zIndexBackwardCompatibility(value: number) { + if (value) { + this._zIndex = value; + } + } + @Input('zIndex') set zIndex(value: number) { + if (value) { + this._zIndex = value; + } + } + get zIndex() { + return this._zIndex; + } + + // Animation duration + @Input('animation-duration') set animationDurationBackwardCompatibility(value: number) { + if (value) { + this._animationDuration = value; + } + } + @Input('animationDuration') set animationDuration(value: number) { + if (value) { + this._animationDuration = value; + } + } + get animationDuration() { + return this._animationDuration; + } + + + @Input('trigger') trigger!: string; + + // Tooltip class + @Input('tooltip-class') set tooltipClassBackwardCompatibility(value: string) { + if (value) { + this._tooltipClass = value; + } + } + @Input('tooltipClass') set tooltipClass(value: string) { + if (value) { + this._tooltipClass = value; + } + } + get tooltipClass() { + return this._tooltipClass; + } + + @Input('display') display!: boolean; + @Input('display-mobile') displayMobile!: boolean; + @Input('displayTouchscreen') displayTouchscreen!: boolean; + @Input('shadow') shadow!: boolean; + @Input('theme') theme!: "dark" | "light"; + @Input('offset') offset!: number; + @Input('width') width!: string; + + // Max width + @Input('max-width') set maxWidthBackwardCompatibility(value: string) { + if (value) { + this._maxWidth = value; + } + } + @Input('maxWidth') set maxWidth(value: string) { + if (value) { + this._maxWidth = value; + } + } + get maxWidth() { + return this._maxWidth; + } + + + @Input('id') id: any; + + // Show delay + @Input('show-delay') set showDelayBackwardCompatibility(value: number) { + if (value) { + this._showDelay = value; + } + } + @Input('showDelay') set showDelay(value: number) { + if (value) { + this._showDelay = value; + } + } + get showDelay() { + return this._showDelay; + } + + // Hide delay + @Input('hide-delay') set hideDelayBackwardCompatibility(value: number) { + if (value) { + this._hideDelay = value; + } + } + @Input('hideDelay') set hideDelay(value: number) { + if (value) { + this._hideDelay = value; + } + } + get hideDelay() { + return this._hideDelay; + } + + @Input('hideDelayAfterClick') hideDelayAfterClick!: number; + @Input('pointerEvents') pointerEvents!: 'auto' | 'none'; + @Input('position') position!: {top: number, left: number}; + + get isTooltipDestroyed() { + return this.componentRef && this.componentRef.hostView.destroyed; + } + + get destroyDelay() { + if (this._destroyDelay) { + return this._destroyDelay; + } else { + return Number(this.getHideDelay()) + Number(this.options['animationDuration']); + } + } + set destroyDelay(value: number) { + this._destroyDelay = value; + } + + get tooltipPosition() { + if (this.options['position']) { + return this.options['position']; + } else { + return this.elementPosition; + } + } + + @Output() events: EventEmitter < any > = new EventEmitter < any > (); + + constructor( + @Optional() @Inject(TooltipOptionsService) private initOptions:any, + private elementRef: ElementRef, + private componentFactoryResolver: ComponentFactoryResolver, + private appRef: ApplicationRef, + private injector: Injector) {} + + @HostListener('focusin') + @HostListener('mouseenter') + onMouseEnter() { + if (this.isDisplayOnHover == false) { + return; + } + + this.show(); + } + + @HostListener('focusout') + @HostListener('mouseleave') + onMouseLeave() { + if (this.options['trigger'] === 'hover') { + this.destroyTooltip(); + } + } + + @HostListener('click') + onClick() { + if (this.isDisplayOnClick == false) { + return; + } + + this.show(); + this.hideAfterClickTimeoutId = window.setTimeout(() => { + this.destroyTooltip(); + }, this.options['hideDelayAfterClick']) + } + + ngOnInit(): void { + } + + ngOnChanges(changes: SimpleChanges) { + this.initOptions = this.renameProperties(this.initOptions); + let changedOptions = this.getProperties(changes); + changedOptions = this.renameProperties(changedOptions); + + this.applyOptionsDefault(defaultOptions, changedOptions); + } + + ngOnDestroy(): void { + this.destroyTooltip({ + fast: true + }); + + if (this.componentSubscribe) { + this.componentSubscribe.unsubscribe(); + } + } + + getShowDelay() { + return this.options['showDelay']; + } + + getHideDelay() { + const hideDelay = this.options['hideDelay']; + const hideDelayTouchscreen = this.options['hideDelayTouchscreen']; + + return this.isTouchScreen ? hideDelayTouchscreen : hideDelay; + } + + getProperties(changes: SimpleChanges){ + let directiveProperties:any = {}; + let customProperties:any = {}; + let allProperties:any = {}; + + for (var prop in changes) { + if (prop !== 'options' && prop !== 'tooltipValue'){ + directiveProperties[prop] = changes[prop].currentValue; + } + if (prop === 'options'){ + customProperties = changes[prop].currentValue; + } + } + + allProperties = Object.assign({}, customProperties, directiveProperties); + return allProperties; + } + + renameProperties(options:any) { + for (var prop in options) { + if (backwardCompatibilityOptions[prop]) { + options[backwardCompatibilityOptions[prop]] = options[prop]; + delete options[prop]; + } + } + + return options; + } + + getElementPosition(): void { + this.elementPosition = this.elementRef.nativeElement.getBoundingClientRect(); + } + + createTooltip(): void { + this.clearTimeouts(); + this.getElementPosition(); + + this.createTimeoutId = window.setTimeout(() => { + this.appendComponentToBody(TooltipComponent); + }, this.getShowDelay()); + + this.showTimeoutId = window.setTimeout(() => { + this.showTooltipElem(); + }, this.getShowDelay()); + } + + destroyTooltip(options = { + fast: false + }): void { + this.clearTimeouts(); + + if (this.isTooltipDestroyed == false) { + this.hideTimeoutId = window.setTimeout(() => { + this.hideTooltip(); + }, options.fast ? 0 : this.getHideDelay()); + + this.destroyTimeoutId = window.setTimeout(() => { + if (!this.componentRef || this.isTooltipDestroyed) { + return; + } + + this.appRef.detachView(this.componentRef.hostView); + this.componentRef.destroy(); + this.events.emit({ + type: 'hidden', + position: this.tooltipPosition + }); + }, options.fast ? 0 : this.destroyDelay); + } + } + + showTooltipElem(): void { + this.clearTimeouts(); + ( < AdComponent > this.componentRef.instance).show = true; + this.events.emit({ + type: 'show', + position: this.tooltipPosition + }); + } + + hideTooltip(): void { + if (!this.componentRef || this.isTooltipDestroyed) { + return; + } + ( < AdComponent > this.componentRef.instance).show = false; + this.events.emit({ + type: 'hide', + position: this.tooltipPosition + }); + } + + appendComponentToBody(component: any, data: any = {}): void { + this.componentRef = this.componentFactoryResolver + .resolveComponentFactory(component) + .create(this.injector); + + ( < AdComponent > this.componentRef.instance).data = { + value: this.tooltipValue, + element: this.elementRef.nativeElement, + elementPosition: this.tooltipPosition, + options: this.options + } + this.appRef.attachView(this.componentRef.hostView); + const domElem = (this.componentRef.hostView as EmbeddedViewRef < any > ).rootNodes[0] as HTMLElement; + document.body.appendChild(domElem); + + this.componentSubscribe = ( < AdComponent > this.componentRef.instance).events.subscribe((event: any) => { + this.handleEvents(event); + }); + } + + clearTimeouts(): void { + if (this.createTimeoutId) { + clearTimeout(this.createTimeoutId); + } + + if (this.showTimeoutId) { + clearTimeout(this.showTimeoutId); + } + + if (this.hideTimeoutId) { + clearTimeout(this.hideTimeoutId); + } + + if (this.destroyTimeoutId) { + clearTimeout(this.destroyTimeoutId); + } + } + + get isDisplayOnHover(): boolean { + if (this.options['display'] == false) { + return false; + } + + if (this.options['displayTouchscreen'] == false && this.isTouchScreen) { + return false; + } + + if (this.options['trigger'] !== 'hover') { + return false; + } + + return true; + } + + get isDisplayOnClick(): boolean { + if (this.options['display'] == false) { + return false; + } + + if (this.options['displayTouchscreen'] == false && this.isTouchScreen) { + return false; + } + + if (this.options['trigger'] != 'click') { + return false; + } + + return true; + } + + get isTouchScreen() { + var prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); + var mq = function(query:any) { + return window.matchMedia(query).matches; + } + + if (('ontouchstart' in window)) { + return true; + } + + // include the 'heartz' as a way to have a non matching MQ to help terminate the join + // https://git.io/vznFH + var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); + return mq(query); + } + + applyOptionsDefault(defaultOptions:any, options:any): void { + this.options = Object.assign({}, defaultOptions, this.initOptions || {}, this.options, options); + } + + handleEvents(event: any) { + if (event.type === 'shown') { + this.events.emit({ + type: 'shown', + position: this.tooltipPosition + }); + } + } + + public show() { + if (!this.tooltipValue) { + return; + } + + if (!this.componentRef || this.isTooltipDestroyed) { + this.createTooltip(); + } else if (!this.isTooltipDestroyed) { + this.showTooltipElem(); + } + } + + public hide() { + this.destroyTooltip(); + } +} diff --git a/CSETWebNg/src/app/tooltip/tooltip.module.ts b/CSETWebNg/src/app/tooltip/tooltip.module.ts new file mode 100644 index 000000000..bd7504a91 --- /dev/null +++ b/CSETWebNg/src/app/tooltip/tooltip.module.ts @@ -0,0 +1,33 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TooltipDirective } from './tooltip.directive'; +import { TooltipComponent } from './tooltip.component'; +import { TooltipOptions } from './options.interface'; +import { TooltipOptionsService } from './options.service'; + +@NgModule({ + declarations: [ + TooltipDirective, + TooltipComponent + ], + imports: [ + CommonModule + ], + exports: [ + TooltipDirective + ] +}) +export class TooltipModule { + + static forRoot(initOptions: TooltipOptions): ModuleWithProviders { + return { + ngModule: TooltipModule, + providers: [ + { + provide: TooltipOptionsService, + useValue: initOptions + } + ] + }; + } +}