UNPKG

@controllable-ui/ngx-tooltip

Version:

> ✨ Check out the demo [here](https://ciprian-anin.github.io/controllable-ui-angular/).

1 lines 79.9 kB
{"version":3,"file":"controllable-ui-ngx-tooltip.mjs","sources":["../../../projects/ngx-tooltip/src/lib/ngx-tooltip-arrow/ngx-tooltip-arrow.component.ts","../../../projects/ngx-tooltip/src/lib/ngx-tooltip-arrow/ngx-tooltip-arrow.component.html","../../../projects/ngx-tooltip/src/lib/utils/utils.ts","../../../projects/ngx-tooltip/src/lib/utils/availablePosition.utils.ts","../../../projects/ngx-tooltip/src/lib/utils/positionStyle.utils.ts","../../../projects/ngx-tooltip/src/lib/ngx-tooltip.component.ts","../../../projects/ngx-tooltip/src/lib/ngx-tooltip.component.html","../../../projects/ngx-tooltip/src/public-api.ts","../../../projects/ngx-tooltip/src/controllable-ui-ngx-tooltip.ts"],"sourcesContent":["import { CommonModule } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n Component,\n input,\n ViewEncapsulation,\n} from '@angular/core';\n\n@Component({\n selector: 'ngx-tooltip-arrow',\n imports: [CommonModule],\n templateUrl: './ngx-tooltip-arrow.component.html',\n styleUrl: './ngx-tooltip-arrow.component.scss',\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None\n})\nexport class NgxTooltipArrowComponent {\n arrowSize = input<number>();\n}\n","<div\n class=\"ngxTooltip-arrow\"\n [ngStyle]=\"{ 'width.px': arrowSize(), 'height.px': arrowSize() }\"\n>\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 -16 32 32\">\n <path d=\"M16 0l16 16H0z\"></path>\n </svg>\n</div>\n","import { Signal } from '@angular/core';\r\n\r\nexport const generateRandomNumber = () => {\r\n const seed = new Date().getTime(); // Get current timestamp\r\n const random = Math.abs(Math.sin(seed) * 1000000); // Use sine function for randomness and scale it\r\n return Math.floor(random); // Return a positive integer\r\n};\r\n\r\nexport const getDocumentWidth = () => document.documentElement.clientWidth;\r\nexport const getDocumentHeight = () => document.documentElement.clientHeight;\r\nexport const nextTickRender = () =>\r\n new Promise((resolve) => setTimeout(resolve));\r\n\r\nexport const delay = (milliseconds: number, timeoutIDSignal: Signal<number>) =>\r\n new Promise((resolve) => setTimeout(resolve, milliseconds));\r\n\r\nexport function getScrollableContainer(scrollableContainer?: HTMLElement) {\r\n return scrollableContainer ?? document.documentElement;\r\n}\r\n","import { Placement } from '../types';\r\nimport { getDocumentHeight, getDocumentWidth } from './utils';\r\n\r\n/**\r\n * In order to avoid the annoying flickering from top to bottom or from left to right\r\n * in case one side size has mare space than other, after we already have dialog placed on a side,\r\n * we should try to keep dialog on current placement until min size is hit.\r\n *\r\n * When it is first opened it should be positioned were is the most space and\r\n * it should remain there, decreasing height as much as we can\r\n * (until we hit min-height / min-width) and after trying to position it\r\n * on opposed side if there is more space (even if it needs a small decrease of height\r\n * on the opposed side, than the default height)\r\n * * (basically trying all placements in the order defined in placementsToBeTried)\r\n *\r\n */\r\nexport function getDialogAvailablePositionConsideringKeepingCurrentPlacement({\r\n placementsToBeTried,\r\n currentPlacement,\r\n dialogElement,\r\n relativeElement,\r\n dialogMaxHeight = Infinity,\r\n dialogMinHeight = 0,\r\n dialogMaxWidth = Infinity,\r\n dialogMinWidth = 0,\r\n}: {\r\n placementsToBeTried: [\r\n preferredPlacement: Placement,\r\n ...restOfPlacements: Placement[]\r\n ];\r\n currentPlacement: Placement | undefined;\r\n dialogElement: HTMLElement;\r\n relativeElement: HTMLElement;\r\n dialogMaxHeight?: number;\r\n dialogMinHeight?: number;\r\n dialogMaxWidth?: number;\r\n dialogMinWidth?: number;\r\n}) {\r\n switch (currentPlacement) {\r\n case 'top-start':\r\n case 'top':\r\n case 'top-end': {\r\n // decrease height to fit on top; if min-height was hit try to place again on placementsToBeTried\r\n const maxAvailableSpaceOnTop = getMaxAvailableSpaceOnTop(relativeElement);\r\n return maxAvailableSpaceOnTop < dialogMinHeight\r\n ? getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried,\r\n dialogElement,\r\n relativeElement\r\n )\r\n : maxAvailableSpaceOnTop > dialogMaxHeight\r\n ? {\r\n type: 'full-size-available' as const,\r\n placement: currentPlacement,\r\n }\r\n : {\r\n type: 'partial-size-available' as const,\r\n placement: currentPlacement,\r\n availableSize: maxAvailableSpaceOnTop,\r\n };\r\n }\r\n case 'bottom-start':\r\n case 'bottom':\r\n case 'bottom-end': {\r\n // decrease height to fit on bottom; if min-height was hit try to place again on placementsToBeTried\r\n const maxAvailableSpaceOnBottom =\r\n getMaxAvailableSpaceOnBottom(relativeElement);\r\n return maxAvailableSpaceOnBottom < dialogMinHeight\r\n ? getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried,\r\n dialogElement,\r\n relativeElement\r\n )\r\n : maxAvailableSpaceOnBottom > dialogMaxHeight\r\n ? {\r\n type: 'full-size-available' as const,\r\n placement: currentPlacement,\r\n }\r\n : {\r\n type: 'partial-size-available' as const,\r\n placement: currentPlacement,\r\n availableSize: maxAvailableSpaceOnBottom,\r\n };\r\n }\r\n case 'left-start':\r\n case 'left':\r\n case 'left-end': {\r\n // decrease height to fit on left; if min-width was hit try to place again on placementsToBeTried\r\n const maxAvailableSpaceOnLeft =\r\n getMaxAvailableSpaceOnLeft(relativeElement);\r\n return maxAvailableSpaceOnLeft < dialogMinWidth\r\n ? getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried,\r\n dialogElement,\r\n relativeElement\r\n )\r\n : maxAvailableSpaceOnLeft > dialogMaxWidth\r\n ? {\r\n type: 'full-size-available' as const,\r\n placement: currentPlacement,\r\n }\r\n : {\r\n type: 'partial-size-available' as const,\r\n placement: currentPlacement,\r\n availableSize: maxAvailableSpaceOnLeft,\r\n };\r\n }\r\n case 'right-start':\r\n case 'right':\r\n case 'right-end': {\r\n // decrease height to fit on right; if min-width was hit try to place again on placementsToBeTried\r\n const maxAvailableSpaceOnRight =\r\n getMaxAvailableSpaceOnRight(relativeElement);\r\n return maxAvailableSpaceOnRight < dialogMinWidth\r\n ? getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried,\r\n dialogElement,\r\n relativeElement\r\n )\r\n : maxAvailableSpaceOnRight > dialogMaxWidth\r\n ? {\r\n type: 'full-size-available' as const,\r\n placement: currentPlacement,\r\n }\r\n : {\r\n type: 'partial-size-available' as const,\r\n placement: currentPlacement,\r\n availableSize: maxAvailableSpaceOnRight,\r\n };\r\n }\r\n default: {\r\n return getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried,\r\n dialogElement,\r\n relativeElement\r\n );\r\n }\r\n }\r\n}\r\n\r\nexport function getAvailablePlacementFromTheOnesToBeTried(\r\n placementsToBeTried: [\r\n preferredPlacement: Placement,\r\n ...restOfPlacements: Placement[]\r\n ],\r\n dialogElement: HTMLElement,\r\n relativeElement: HTMLElement\r\n) {\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n const relativeElementRect = relativeElement.getBoundingClientRect();\r\n\r\n const placementWithFullAvailableSpace = placementsToBeTried.find(\r\n (placementToBeTried) => {\r\n switch (placementToBeTried) {\r\n case 'top-start':\r\n case 'top':\r\n case 'top-end': {\r\n return relativeElementRect.top - dialogElementRect.height >= 0;\r\n }\r\n case 'bottom-start':\r\n case 'bottom':\r\n case 'bottom-end': {\r\n return (\r\n relativeElementRect.bottom + dialogElementRect.height <=\r\n getDocumentHeight()\r\n );\r\n }\r\n case 'left-start':\r\n case 'left':\r\n case 'left-end': {\r\n return relativeElementRect.left - dialogElementRect.width >= 0;\r\n }\r\n case 'right-start':\r\n case 'right':\r\n case 'right-end': {\r\n return (\r\n relativeElementRect.right + dialogElementRect.width <=\r\n getDocumentWidth()\r\n );\r\n }\r\n }\r\n }\r\n );\r\n\r\n if (placementWithFullAvailableSpace) {\r\n return {\r\n type: 'full-size-available' as const,\r\n placement: placementWithFullAvailableSpace,\r\n };\r\n } else {\r\n return {\r\n type: 'partial-size-available' as const,\r\n ...getMaxPartialAvailableSpace(placementsToBeTried, relativeElement),\r\n };\r\n }\r\n}\r\n\r\nfunction getMaxPartialAvailableSpace(\r\n placementsToBeTried: [\r\n preferredPlacement: Placement,\r\n ...restOfPlacements: Placement[]\r\n ],\r\n relativeElement: HTMLElement\r\n) {\r\n const availableSizeOfPlacementsToBeTried = placementsToBeTried.map(\r\n (currentPlacementToBeTried) => ({\r\n placement: currentPlacementToBeTried,\r\n availableSize: computeMaxAvailableSizeOfDialogForPlacement(\r\n currentPlacementToBeTried,\r\n relativeElement\r\n ),\r\n })\r\n );\r\n\r\n const placementWithMaxAvailableSpace =\r\n availableSizeOfPlacementsToBeTried.reduce<{\r\n placement: Placement;\r\n availableSize: number;\r\n }>((acc, value) => {\r\n return acc.availableSize >= value.availableSize ? acc : value;\r\n }, availableSizeOfPlacementsToBeTried[0]);\r\n\r\n return placementWithMaxAvailableSpace;\r\n}\r\n\r\nfunction getMaxAvailableSpaceOnTop(relativeElement: HTMLElement) {\r\n const relativeElementRect = relativeElement.getBoundingClientRect();\r\n\r\n return relativeElementRect.top;\r\n}\r\n\r\nfunction getMaxAvailableSpaceOnBottom(relativeElement: HTMLElement) {\r\n const relativeElementRect = relativeElement.getBoundingClientRect();\r\n\r\n return getDocumentHeight() - relativeElementRect.bottom;\r\n}\r\n\r\nfunction getMaxAvailableSpaceOnLeft(relativeElement: HTMLElement) {\r\n const relativeElementRect = relativeElement.getBoundingClientRect();\r\n\r\n return relativeElementRect.left;\r\n}\r\n\r\nfunction getMaxAvailableSpaceOnRight(relativeElement: HTMLElement) {\r\n const relativeElementRect = relativeElement.getBoundingClientRect();\r\n\r\n return getDocumentWidth() - relativeElementRect.right;\r\n}\r\n\r\nfunction computeMaxAvailableSizeOfDialogForPlacement(\r\n placement: Placement,\r\n relativeElement: HTMLElement\r\n) {\r\n switch (placement) {\r\n case 'top-start':\r\n case 'top':\r\n case 'top-end': {\r\n return getMaxAvailableSpaceOnTop(relativeElement);\r\n }\r\n case 'bottom-start':\r\n case 'bottom':\r\n case 'bottom-end': {\r\n return getMaxAvailableSpaceOnBottom(relativeElement);\r\n }\r\n case 'left-start':\r\n case 'left':\r\n case 'left-end': {\r\n return getMaxAvailableSpaceOnLeft(relativeElement);\r\n }\r\n case 'right-start':\r\n case 'right':\r\n case 'right-end': {\r\n return getMaxAvailableSpaceOnRight(relativeElement);\r\n }\r\n }\r\n}\r\n","import { Placement } from '../types';\r\nimport {\r\n getDocumentHeight,\r\n getDocumentWidth,\r\n} from './utils';\r\n\r\nexport function getBottomEndPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { right, bottom } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustBottomToBeAtMostAtTheBottomEdgeOfScrollableContainer(\r\n bottom,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n right - dialogElementRect.width,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getBottomPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, bottom, width } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustBottomToBeAtMostAtTheBottomEdgeOfScrollableContainer(\r\n bottom,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n x + width / 2 - dialogElementRect.width / 2,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getBottomStartPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, bottom } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustBottomToBeAtMostAtTheBottomEdgeOfScrollableContainer(\r\n bottom,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n x,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getRightEndPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { right, bottom } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n bottom - dialogElementRect.height,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustRightToBeAtMostAtTheRightEdgeOfScrollableContainer(\r\n right,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getRightPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { right, y, height } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n y + height / 2 - dialogElementRect.height / 2,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustRightToBeAtMostAtTheRightEdgeOfScrollableContainer(\r\n right,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getRightStartPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { right, y } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n y,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${ensureAdjustRightToBeAtMostAtTheRightEdgeOfScrollableContainer(\r\n right,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getLeftEndPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, bottom } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n bottom - dialogElementRect.height,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${\r\n ensureAdjustXToBeAtMostAtTheLeftEdgeOfScrollableContainer(\r\n x,\r\n scrollableContainer\r\n ) - dialogElementRect.width\r\n }px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getLeftPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, y, height } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n y + height / 2 - dialogElementRect.height / 2,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${\r\n ensureAdjustXToBeAtMostAtTheLeftEdgeOfScrollableContainer(\r\n x,\r\n scrollableContainer\r\n ) - dialogElementRect.width\r\n }px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getLeftStartPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, y } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${ensureAdjustYToHaveHeightInsideView(\r\n y,\r\n dialogElementRect.height,\r\n scrollableContainer\r\n )}px`,\r\n left: `${\r\n ensureAdjustXToBeAtMostAtTheLeftEdgeOfScrollableContainer(\r\n x,\r\n scrollableContainer\r\n ) - dialogElementRect.width\r\n }px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getTopEndPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n}: {\r\n relativeElement: HTMLElement;\r\n dialogElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { y, right } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${\r\n ensureAdjustYToBeAtMostAtTheTopEdgeOfScrollableContainer(\r\n y,\r\n scrollableContainer\r\n ) - dialogElementRect.height\r\n }px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n right - dialogElementRect.width,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function ensureAdjustXToHaveWidthInsideView(\r\n x: number,\r\n dialogWidth: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n const rightEdgeOfView = scrollableContainer\r\n ? Math.min(\r\n getDocumentWidth(),\r\n scrollableContainer.getBoundingClientRect().right\r\n )\r\n : getDocumentWidth();\r\n\r\n const getLeftEdgeOfView = () =>\r\n scrollableContainer\r\n ? Math.max(0, scrollableContainer.getBoundingClientRect().left)\r\n : 0;\r\n\r\n const sizeOutsideRightSide = x + dialogWidth - rightEdgeOfView;\r\n const adjustedX =\r\n sizeOutsideRightSide > 0\r\n ? Math.max(x - sizeOutsideRightSide, 0)\r\n : Math.max(x, getLeftEdgeOfView());\r\n\r\n return Math.round(adjustedX);\r\n}\r\n\r\nexport function ensureAdjustYToBeAtMostAtTheTopEdgeOfScrollableContainer(\r\n y: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n return scrollableContainer\r\n ? Math.max(y, scrollableContainer?.getBoundingClientRect().y)\r\n : y;\r\n}\r\n\r\nexport function ensureAdjustBottomToBeAtMostAtTheBottomEdgeOfScrollableContainer(\r\n bottom: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n return scrollableContainer\r\n ? Math.min(bottom, scrollableContainer.getBoundingClientRect().bottom)\r\n : bottom;\r\n}\r\n\r\nexport function ensureAdjustXToBeAtMostAtTheLeftEdgeOfScrollableContainer(\r\n x: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n return scrollableContainer\r\n ? Math.max(x, scrollableContainer.getBoundingClientRect().x)\r\n : x;\r\n}\r\n\r\nexport function ensureAdjustRightToBeAtMostAtTheRightEdgeOfScrollableContainer(\r\n right: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n return scrollableContainer\r\n ? Math.min(right, scrollableContainer.getBoundingClientRect().right)\r\n : right;\r\n}\r\n\r\nexport function ensureAdjustYToHaveHeightInsideView(\r\n y: number,\r\n dialogHeight: number,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n const bottomEdgeOfView = scrollableContainer\r\n ? Math.min(\r\n getDocumentHeight(),\r\n scrollableContainer.getBoundingClientRect().bottom\r\n )\r\n : getDocumentHeight();\r\n\r\n const getTopEdgeOfView = () =>\r\n scrollableContainer\r\n ? Math.max(0, scrollableContainer.getBoundingClientRect().top)\r\n : 0;\r\n\r\n const sizeOutside = y + dialogHeight - bottomEdgeOfView;\r\n const adjustedY =\r\n sizeOutside > 0\r\n ? Math.max(y - sizeOutside, 0)\r\n : Math.max(y, getTopEdgeOfView());\r\n\r\n return adjustedY;\r\n}\r\n\r\nexport function getTopStartPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n}: {\r\n dialogElement: HTMLElement;\r\n relativeElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, y } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${\r\n ensureAdjustYToBeAtMostAtTheTopEdgeOfScrollableContainer(\r\n y,\r\n scrollableContainer\r\n ) - dialogElementRect.height\r\n }px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n x,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getTopPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n}: {\r\n dialogElement: HTMLElement;\r\n relativeElement: HTMLElement;\r\n scrollableContainer?: HTMLElement;\r\n}) {\r\n const { x, y, width } = relativeElement.getBoundingClientRect();\r\n const dialogElementRect = dialogElement.getBoundingClientRect();\r\n\r\n return {\r\n top: `${\r\n ensureAdjustYToBeAtMostAtTheTopEdgeOfScrollableContainer(\r\n y,\r\n scrollableContainer\r\n ) - dialogElementRect.height\r\n }px`,\r\n left: `${ensureAdjustXToHaveWidthInsideView(\r\n x + width / 2 - dialogElementRect.width / 2,\r\n dialogElementRect.width,\r\n scrollableContainer\r\n )}px`,\r\n right: 'auto',\r\n bottom: 'auto',\r\n } as const;\r\n}\r\n\r\nexport function getDialogPositionStyle(\r\n availablePosition:\r\n | {\r\n type: 'full-size-available';\r\n placement: Placement;\r\n }\r\n | {\r\n placement: Placement;\r\n availableSize: number;\r\n type: 'partial-size-available';\r\n },\r\n dialogElement: HTMLElement,\r\n relativeElement: HTMLElement,\r\n scrollableContainer?: HTMLElement\r\n) {\r\n switch (availablePosition.placement) {\r\n case 'top-start':\r\n return getTopStartPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'top':\r\n return getTopPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'top-end':\r\n return getTopEndPositionStyle({\r\n relativeElement,\r\n dialogElement,\r\n scrollableContainer,\r\n });\r\n case 'bottom-start':\r\n return getBottomStartPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'bottom':\r\n return getBottomPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'bottom-end':\r\n return getBottomEndPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n case 'left-start':\r\n return getLeftStartPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'left':\r\n return getLeftPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'left-end':\r\n return getLeftEndPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n case 'right-start':\r\n return getRightStartPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'right':\r\n return getRightPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n\r\n case 'right-end':\r\n return getRightEndPositionStyle({\r\n dialogElement,\r\n relativeElement,\r\n scrollableContainer,\r\n });\r\n }\r\n}\r\n","import { CommonModule } from '@angular/common';\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n effect,\r\n ElementRef,\r\n input,\r\n OnChanges,\r\n output,\r\n signal,\r\n Signal,\r\n SimpleChanges,\r\n viewChild,\r\n ViewEncapsulation,\r\n} from '@angular/core';\r\n\r\nimport { NgxTooltipArrowComponent } from './ngx-tooltip-arrow/ngx-tooltip-arrow.component';\r\nimport { Placement } from './types';\r\nimport {\r\n getAvailablePlacementFromTheOnesToBeTried,\r\n getDialogAvailablePositionConsideringKeepingCurrentPlacement,\r\n} from './utils/availablePosition.utils';\r\nimport { getDialogPositionStyle } from './utils/positionStyle.utils';\r\nimport {\r\n generateRandomNumber,\r\n getScrollableContainer,\r\n nextTickRender,\r\n} from './utils/utils';\r\n\r\nexport const defaultOrderOfPlacementsToBeTried: {\r\n [key in Placement]: [\r\n preferredPlacement: Placement,\r\n ...restOfPlacements: Placement[]\r\n ];\r\n} = {\r\n 'top-start': ['top-start', 'bottom-start', 'left', 'right'],\r\n top: ['top', 'bottom', 'left', 'right'],\r\n 'top-end': ['top-end', 'bottom-end', 'left', 'right'],\r\n\r\n 'left-start': ['left-start', 'right-start', 'top', 'bottom'],\r\n left: ['left', 'right', 'top', 'bottom'],\r\n 'left-end': ['left-end', 'right-end', 'top', 'bottom'],\r\n\r\n 'right-start': ['right-start', 'left-start', 'top', 'bottom'],\r\n right: ['right', 'left', 'top', 'bottom'],\r\n 'right-end': ['right-end', 'left-end', 'top', 'bottom'],\r\n\r\n 'bottom-start': ['bottom-start', 'top-start', 'left', 'right'],\r\n bottom: ['bottom', 'top', 'left', 'right'],\r\n 'bottom-end': ['bottom-end', 'top-end', 'left', 'right'],\r\n};\r\n\r\ntype BaseProps = {\r\n open: Signal<boolean>;\r\n onOpen$?: () => void;\r\n onClose$?: () => void;\r\n preferredPlacement?: Placement;\r\n orderOfPlacementsToBeTried?: [\r\n preferredPlacement: Placement,\r\n ...restOfPlacements: Placement[]\r\n ];\r\n triggerActions?: ('hover' | 'focus' | 'click')[];\r\n /**\r\n * Distance between relative element and tooltip dialog\r\n */\r\n dialogOffset?: number;\r\n /**\r\n * Open timeout in ms\r\n */\r\n enterDelay?: number;\r\n /**\r\n * Close timeout in ms\r\n */\r\n leaveDelay?: number;\r\n arrow?: boolean;\r\n /**\r\n * Scrollable container is the one use\r\n * to track scroll event and position dialog while scrolling inside it.\r\n */\r\n scrollableContainer?: HTMLElement;\r\n tooltipClass?: string;\r\n tooltipRootClass?: string;\r\n};\r\n\r\nexport type KeepCurrentPlacementStrategyProps = BaseProps & {\r\n /**\r\n * Keep current placement of dialog as time as it remains in\r\n * min & max sizes boundaries.\r\n */\r\n placementStrategy: 'considerKeepingCurrentPlacement';\r\n /**\r\n * `dialogMinMaxSizes`:\r\n * > In case we need to keep current position, we will use maximum & minimum sizes\r\n * > of dialog to check if it fits in current placement, without going over its minimum sizes.\r\n * > In case we don't have minimum size available for current placement,\r\n * > than will be tried next place from `orderOfPlacementsToBeTried`.\r\n *\r\n * > Maximum size is used to make sure to not have a bigger maximum size on dialog popover.\r\n * > (we make sure to override the maximum size in case the available space is smaller than the dialog size)\r\n */\r\n dialogMinMaxSizes?: {\r\n dialogMaxHeight?: number;\r\n dialogMinHeight?: number;\r\n dialogMaxWidth?: number;\r\n dialogMinWidth?: number;\r\n };\r\n};\r\n\r\n/**\r\n * Dialog placement will be recomputed immediately after we remain\r\n * without necessary space for dialog on current placement.\r\n */\r\nexport type DefaultStrategyProps = BaseProps & {\r\n placementStrategy: 'default';\r\n};\r\n\r\nexport type Props = DefaultStrategyProps | KeepCurrentPlacementStrategyProps;\r\n\r\n@Component({\r\n selector: 'ngx-tooltip',\r\n imports: [CommonModule, NgxTooltipArrowComponent],\r\n templateUrl: './ngx-tooltip.component.html',\r\n styleUrl: './ngx-tooltip.component.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.None,\r\n})\r\nexport class NgxTooltipComponent implements OnChanges {\r\n open = input<boolean>();\r\n\r\n preferredPlacement = input<Placement>('bottom-start');\r\n orderOfPlacementsToBeTried =\r\n input<[preferredPlacement: Placement, ...restOfPlacements: Placement[]]>();\r\n orderOfPlacementsToBeTriedComputed = computed(\r\n () =>\r\n this.orderOfPlacementsToBeTried() ||\r\n defaultOrderOfPlacementsToBeTried[this.preferredPlacement()]\r\n );\r\n\r\n enterDelay = input<number>(100);\r\n leaveDelay = input<number>(150);\r\n triggerActions = input<('hover' | 'focus' | 'click')[]>(['hover', 'focus']);\r\n arrow = input<boolean>(false);\r\n arrowSize = computed<number>(() => (this.arrow() ? 12 : 0));\r\n dialogOffset = input<number>(5);\r\n computedDialogOffset = computed(() =>\r\n this.arrow()\r\n ? this.dialogOffset() - this.arrowSize() / 2\r\n : this.dialogOffset()\r\n );\r\n bridgeSize = computed<number>(\r\n () => this.computedDialogOffset() + this.arrowSize()\r\n );\r\n\r\n scrollableContainer = input<ElementRef>();\r\n tooltipRootClass = input<string>();\r\n tooltipClass = input<string>();\r\n\r\n /**\r\n * default - Dialog placement will be recomputed immediately after we remain\r\n * without necessary space for dialog on current placement.\r\n *\r\n * considerKeepingCurrentPlacement - Keep current placement of dialog as time as it remains in\r\n * min & max sizes boundaries.\r\n */\r\n placementStrategy = input<\r\n 'default' | 'considerKeepingCurrentPlacement' | undefined\r\n >('default');\r\n\r\n /**\r\n *\r\n * `dialogMinMaxSizes`:\r\n * > is used when placementStrategy has value considerKeepingCurrentPlacement\r\n * > In case we need to keep current position, we will use maximum & minimum sizes\r\n * > of dialog to check if it fits in current placement, without going over its minimum sizes.\r\n * > In case we don't have minimum size available for current placement,\r\n * > than will be tried next place from `orderOfPlacementsToBeTried`.\r\n * * > Maximum size is used to make sure to not have a bigger maximum size on dialog popover.\r\n * > (we make sure to override the maximum size in case the available space is smaller than the dialog size)\r\n */\r\n dialogMinMaxSizes = input<{\r\n dialogMaxHeight?: number;\r\n dialogMinHeight?: number;\r\n dialogMaxWidth?: number;\r\n dialogMinWidth?: number;\r\n }>();\r\n\r\n onOpen$ = output();\r\n onClose$ = output();\r\n\r\n tooltipId = generateRandomNumber();\r\n\r\n relativeElementRef = viewChild<ElementRef>('relativeElementRef');\r\n dialogWithBridgeRef = viewChild<ElementRef>('dialogWithBridgeRef');\r\n dialogRef = viewChild<ElementRef>('dialogRef');\r\n\r\n dialogIsOpenLocalState = signal(this.open());\r\n dialogPositionStyle = signal<{\r\n currentPlacement?: Placement;\r\n value: {\r\n visibility?: string;\r\n bottom?: string;\r\n top?: string;\r\n left?: string;\r\n right?: string;\r\n '--scrollbar-height'?: string;\r\n '--relative-x'?: string;\r\n '--relative-y'?: string;\r\n '--relative-width'?: string;\r\n '--relative-height'?: string;\r\n '--dialog-x'?: string;\r\n '--dialog-y'?: string;\r\n '--dialog-width'?: string;\r\n '--dialog-height'?: string;\r\n };\r\n maxHeight: string;\r\n maxWidth: string;\r\n '--scrollbar-height'?: string;\r\n }>({\r\n currentPlacement: undefined,\r\n value: {},\r\n maxHeight: '',\r\n maxWidth: '',\r\n });\r\n closeDialogTimeoutID = signal<NodeJS.Timeout | undefined>(undefined);\r\n openDialogTimeoutID = signal<NodeJS.Timeout | undefined>(undefined);\r\n dialogAnimationState = signal<'show' | 'hide' | 'initial'>('initial');\r\n\r\n constructor() {\r\n effect((onCleanup) => {\r\n if (\r\n this.dialogIsOpenLocalState() &&\r\n this.triggerActions()?.includes('click')\r\n ) {\r\n window.addEventListener('click', this.handleClickOutsideClose);\r\n\r\n onCleanup(() => {\r\n window.removeEventListener('click', this.handleClickOutsideClose);\r\n });\r\n }\r\n });\r\n\r\n effect((onCleanup) => {\r\n if (this.dialogIsOpenLocalState()) {\r\n const scrollableContainer = getScrollableContainer(\r\n this.scrollableContainer()?.nativeElement\r\n );\r\n\r\n scrollableContainer.addEventListener('scroll', this.positionDialog);\r\n\r\n onCleanup(() => {\r\n scrollableContainer.removeEventListener(\r\n 'scroll',\r\n this.positionDialog\r\n );\r\n });\r\n }\r\n });\r\n }\r\n\r\n positionDialog = async () => {\r\n if (\r\n this.dialogWithBridgeRef() &&\r\n this.dialogRef() &&\r\n this.relativeElementRef()\r\n ) {\r\n // compute placement of dialog using the dialogRef which doesn't include bridge,\r\n // which is unknown before knowing the next dialog placement.\r\n // We use dialogOffset to properly take the bridge into account when computing the available space & placement\r\n const availablePosition =\r\n this.placementStrategy() === 'considerKeepingCurrentPlacement'\r\n ? getDialogAvailablePositionConsideringKeepingCurrentPlacement({\r\n placementsToBeTried: this.orderOfPlacementsToBeTriedComputed(),\r\n dialogElement: this.dialogRef()!.nativeElement,\r\n relativeElement: this.relativeElementRef()!.nativeElement,\r\n currentPlacement: this.dialogPositionStyle().currentPlacement,\r\n ...this.dialogMinMaxSizes?.(),\r\n })\r\n : getAvailablePlacementFromTheOnesToBeTried(\r\n this.orderOfPlacementsToBeTriedComputed(),\r\n this.dialogRef()!.nativeElement,\r\n this.relativeElementRef()!.nativeElement\r\n );\r\n\r\n if (availablePosition.type === 'partial-size-available') {\r\n // in case of partial size available to be displayed,\r\n // apply the new height or width on element before computing\r\n // its position for available placement location\r\n // (for ex. if placement is left this will make sure to always\r\n // place on left center based on new dialog height obtained\r\n // after reducing the width of it)\r\n switch (availablePosition.placement) {\r\n case 'top-start':\r\n case 'top':\r\n case 'top-end':\r\n case 'bottom-start':\r\n case 'bottom':\r\n case 'bottom-end':\r\n // Note: maxHeight will be set on .ngxTooltip-tooltip\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n maxHeight: `${\r\n availablePosition.availableSize - this.bridgeSize()\r\n }px`,\r\n });\r\n break;\r\n case 'left-start':\r\n case 'left':\r\n case 'left-end':\r\n case 'right-start':\r\n case 'right':\r\n case 'right-end':\r\n // Note: maxWidth will be set on .ngxTooltip-dialog-with-bridge\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n maxWidth: `${availablePosition.availableSize}px`,\r\n });\r\n break;\r\n }\r\n\r\n await nextTickRender(); // wait for new width/height to be rendered,\r\n // to be able to compute properly, the position of dialog for\r\n // available placement location\r\n }\r\n\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n currentPlacement: availablePosition.placement,\r\n });\r\n\r\n await nextTickRender(); // wait for the placement class to be rendered,\r\n // in order to have the bridge (padding) applied\r\n\r\n const dialogPositionStyleProps = getDialogPositionStyle(\r\n availablePosition,\r\n this.dialogWithBridgeRef()!.nativeElement, // dialogWithBridgeRef have the bridge in place in this point\r\n // so we can properly compute the position, taking into account the bridge\r\n this.relativeElementRef()!.nativeElement,\r\n this.scrollableContainer()?.nativeElement\r\n );\r\n\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n value: {\r\n ...this.dialogPositionStyle().value,\r\n ...dialogPositionStyleProps,\r\n '--scrollbar-height': `${\r\n window.innerHeight - document.documentElement.clientHeight\r\n }px`,\r\n // visibility: \"visible\",\r\n },\r\n });\r\n\r\n await nextTickRender(); // wait for the dialog to be rendered on the computed placement\r\n\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n value: {\r\n ...this.dialogPositionStyle().value,\r\n ...(this.arrow()\r\n ? {\r\n ...(() => {\r\n const { x, y, width, height, right, bottom } =\r\n this.relativeElementRef()?.nativeElement.getBoundingClientRect() ??\r\n {};\r\n\r\n const scrollableContainerRect = getScrollableContainer(\r\n this.scrollableContainer()?.nativeElement\r\n ).getBoundingClientRect();\r\n\r\n return {\r\n '--relative-x': `${Math.max(\r\n x,\r\n scrollableContainerRect.x\r\n )}px`,\r\n '--relative-y': `${Math.max(\r\n y,\r\n scrollableContainerRect.y\r\n )}px`,\r\n '--relative-width': `${Math.min(\r\n width,\r\n Math.max(\r\n 0,\r\n width -\r\n Math.max(0, right - scrollableContainerRect.right) -\r\n Math.max(0, scrollableContainerRect.x - x)\r\n )\r\n )}px`,\r\n '--relative-height': `${Math.min(\r\n height,\r\n Math.max(\r\n 0,\r\n height -\r\n Math.max(0, bottom - scrollableContainerRect.bottom) -\r\n Math.max(0, scrollableContainerRect.y - y)\r\n )\r\n )}px`,\r\n };\r\n })(),\r\n ...(() => {\r\n const { x, y, width, height } =\r\n this.dialogRef()?.nativeElement.getBoundingClientRect() ??\r\n {};\r\n\r\n return {\r\n '--dialog-x': `${x}px`,\r\n '--dialog-y': `${y}px`,\r\n '--dialog-width': `${width}px`,\r\n '--dialog-height': `${height}px`,\r\n };\r\n })(),\r\n }\r\n : {}),\r\n },\r\n });\r\n }\r\n };\r\n\r\n scheduleDialogOpen = async () => {\r\n // ! check again to make sure we open dialog just if external\r\n // ! state specify now that the dialog should be opened\r\n // This check is needed because due to asynchronously downloading\r\n // the function, it might be downloaded later than scheduleDialogClose\r\n // an in that case close would take place before scheduleDialogOpen,\r\n // and this will end up, having dialog open, even if external state\r\n // specify that it should be closed\r\n if (this.open()) {\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n value: {\r\n ...this.dialogPositionStyle().value,\r\n visibility: 'hidden',\r\n },\r\n });\r\n\r\n this.openDialogTimeoutID.set(\r\n setTimeout(async () => {\r\n // ! check again to make sure we open dialog just if external\r\n // ! state specify now that the dialog should be opened\r\n\r\n if (this.open()) {\r\n await nextTickRender(); // wait for dialog to have `visibility: hidden` set\r\n // before showing it\r\n // This is important in order to avoid the display of it on a position\r\n // inappropriate with requested/available placement\r\n // * (at this moment we don't have the dialog sizes,\r\n // * and it is not positioned on requested/available placement)\r\n\r\n const positionDialogAndMakeItVisible = new ResizeObserver(\r\n async () => {\r\n await this.positionDialog();\r\n await this.positionDialog(); // call a second time to make sure that the size of dialog is computed properly\r\n // (the first time when we call positionDialog the browser doesn't compute the height/width of dialog properly)\r\n\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n value: {\r\n ...this.dialogPositionStyle().value,\r\n visibility: 'visible',\r\n },\r\n });\r\n\r\n this.dialogAnimationState.set('show');\r\n\r\n positionDialogAndMakeItVisible.disconnect();\r\n }\r\n );\r\n\r\n if (this.dialogRef()) {\r\n positionDialogAndMakeItVisible.observe(\r\n this.dialogRef()!.nativeElement\r\n );\r\n this.dialogWithBridgeRef()?.nativeElement.showPopover();\r\n this.dialogIsOpenLocalState.set(true);\r\n }\r\n }\r\n }, this.enterDelay())\r\n );\r\n }\r\n };\r\n\r\n closeDialog = () => {\r\n // ! check again to make sure we close dialog just if external\r\n // ! state specify now that the dialog should be closed\r\n if (!this.open()) {\r\n this.dialogWithBridgeRef()?.nativeElement.hidePopover();\r\n this.dialogIsOpenLocalState.set(false);\r\n\r\n this.dialogPositionStyle.set({\r\n ...this.dialogPositionStyle(),\r\n currentPlacement: undefined,\r\n value: {},\r\n maxHeight: '',\r\n maxWidth: '',\r\n });\r\n\r\n const scrollableContainer = getScrollableContainer(\r\n this.scrollableContainer()?.nativeElement\r\n );\r\n scrollableContainer.removeEventListener('scroll', this.positionDialog);\r\n }\r\n };\r\n\r\n scheduleDialogClose = () => {\r\n // check again to make sure we close dialog just if external\r\n // state specify now that the dialog should be closed\r\n if (!this.open()) {\r\n this.dialogAnimationState.set('hide');\r\n\r\n this.closeDialogTimeoutID.set(\r\n setTimeout(() => {\r\n this.closeDialog();\r\n }, this.leaveDelay())\r\n );\r\n }\r\n };\r\n\r\n cancelDialogOpen = () => {\r\n clearTimeout(this.openDialogTimeoutID());\r\n this.openDialogTimeoutID.set(undefined);\r\n this.dialogAnimationState.set('hide');\r\n };\r\n\r\n cancelDialogClose = () => {\r\n clearTimeout(this.closeDialogTimeoutID());\r\n this.closeDialogTimeoutID.set(undefined);\r\n this.dialogAnimationState.set('show');\r\n };\r\n\r\n handleClickOutsideClose = async (event: Event) => {\r\n if (\r\n this.dialogWithBridgeRef()?.nativeElement !== event.target &&\r\n !this.dialogWithBridgeRef()?.nativeElement.contains(\r\n event.target as Node\r\n ) &&\r\n this.relativeElementRef()?.nativeElement !== event.target &&\r\n !this.relativeElementRef()?.nativeElement.contains(event.target as Node)\r\n ) {\r\n this.onClose$?.emit();\r\n }\r\n };\r\n\r\n handleOpenAction = () => {\r\n this.onOpen$?.emit();\r\n };\r\n\r\n async ngOnChanges(changes: SimpleChanges) {\r\n if ('open' in changes) {\r\n await this.toggleVisibility();\r\n }\r\n }\r\n\r\n private toggleVisibility = async () => {\r\n const shouldDialogOpen = this.open();\r\n\r\n if (shouldDialogOpen) {\r\n this.cancelDialogClose();\r\n await this.scheduleDialogOpen();\r\n } else {\r\n this.cancelDialogOpen();\r\n if (this.dialogIsOpenLocalState()) {\r\n this.scheduleDialogClose();\r\n }\r\n }\r\n };\r\n\r\n handleRelativeElementMouseOrFocusLeave = (event: MouseEvent | FocusEvent) => {\r\n if (\r\n this.dialogWithBridgeRef()?.nativeElement !== event.relatedTarget &&\r\n (!(event.relatedTarget instanceof Node) ||\r\n !this.dialogWithBridgeRef()?.nativeElement.contains(\r\n event.relatedTarget\r\n ))\r\n ) {\r\n this.onClose$.emit();\r\n }\r\n };\r\n\r\n handleMouseOrFocusLeaveDialog = async (event: MouseEvent | FocusEvent) => {\r\n if (\r\n this.relativeElementRef()?.nativeElement !== event.relatedTarget &&\r\n (!(event.relatedTarget instanceof Node) ||\r\n !this.relativeElementRef()?.nativeElement.contains(event.relatedTarget))\r\n ) {\r\n this.onClose$.emit();\r\n }\r\n };\r\n\r\n handleMouseEnter = async () => {\r\n this.onOpen$?.emit(); // emit open to parent to make sure it will have open state also\r\n await this.cancelDialogClose();\r\n };\r\n}\r\n","<div class=\"ngxTooltip-root-container\">\n <div\n #relativeElementRef\n class=\"ngxTooltip-relative-element\"\n [attr.popovertarget]=\"tooltipId\"\n (mouseenter)=\"\n triggerActions().includes('hover') ? handleOpenAction() : undefined\n \"\n (mouseleave)=\"\n triggerActions().includes('hover')\n ? handleRelativeElementMouseOrFocusLeave($event)\n : undefined\n \"\n (focusin)=\"\n triggerActions().includes('focus') ? handleOpenAction() : undefined\n \"\n (focusout)=\"\n triggerActions().includes('focus')\n ? handleRelativeElementMouseOrFocusLeave($event)\n : undefined\n \"\n (click)=\"\n triggerActions().includes('click') ? handleOpenAction() : undefined\n \"\n >\n <ng-content select=\".relative-element\"></ng-content>\n </div>\n\n <div\n class=\"ngxTooltip-dialog-with-bridge {{ tooltipRootClass() }}\"\n [ngClass]=\"[\n dialogPositionStyle().currentPlacement\n ? 'ngxTooltip-placement-' + dialogPositionStyle().currentPlacement\n : '',\n dialogAnimationState() === 'initial' ? 'ngxTooltip-initial' : '',\n dialogAnimationState() === 'show' ? 'ngxTooltip-show' : '',\n dialogAnimationState() === 'hide' ? 'ngxTooltip-hide' : ''\n ]\"\n #dialogWithBridgeRef\n popover=\"manual\"\n [attr.id]=\"tooltipId\"\n role=\"tooltip\"\n [attr.data-dialog-placement]=\"preferredPlacement()\"\n [ngStyle]=\"{\n 'visibility': dialogPositionStyle().value['visibility'] ?? '',\n 'bottom': dialogPositionStyle().value['bottom'] ?? '',\n 'top': dialogPositionStyle().value['top'] ?? '',\n 'left': dialogPositionStyle(