@syncfusion/ej2-popups
Version:
A package of Essential JS 2 popup components such as Dialog and Tooltip that is used to display information or messages in separate pop-ups.
1,092 lines (1,065 loc) • 77.4 kB
text/typescript
import { Component, Property, ChildProperty, Event, BaseEventArgs, append, compile } from '@syncfusion/ej2-base';
import { EventHandler, EmitType, Touch, TapEventArgs, Browser, Animation as PopupAnimation, animationMode } from '@syncfusion/ej2-base';
import { isNullOrUndefined, getUniqueID, formatUnit, select, selectAll } from '@syncfusion/ej2-base';
import { attributes, closest, removeClass, addClass, remove } from '@syncfusion/ej2-base';
import { NotifyPropertyChanges, INotifyPropertyChanged, Complex, SanitizeHtmlHelper } from '@syncfusion/ej2-base';
import { Popup } from '../popup/popup';
import { OffsetPosition, calculatePosition } from '../common/position';
import { isCollide, fit, destroy as collisionDestroy } from '../common/collision';
import { TooltipModel, AnimationModel } from './tooltip-model';
/**
* Set of open modes available for Tooltip.
* ```props
* Auto :- The tooltip opens automatically when the trigger element is hovered over.
* Hover :- The tooltip opens when the trigger element is hovered over.
* Click :- The tooltip opens when the trigger element is clicked.
* Focus :- The tooltip opens when the trigger element is focused.
* Custom :- The tooltip opens when the trigger element is triggered by a custom event.
* ```
*/
export type OpenMode = 'Auto' | 'Hover' | 'Click' | 'Focus' | 'Custom';
/**
* Applicable positions where the Tooltip can be displayed over specific target elements.
* ```props
* TopLeft :- The tooltip is positioned at the top-left corner of the trigger element.
* TopCenter :- The tooltip is positioned at the top-center of the trigger element.
* TopRight :- The tooltip is positioned at the top-right corner of the trigger element.
* BottomLeft :- The tooltip is positioned at the bottom-left corner of the trigger element.
* BottomCenter :- The tooltip is positioned at the bottom-center of the trigger element.
* BottomRight :- The tooltip is positioned at the bottom-right corner of the trigger element.
* LeftTop :- The tooltip is positioned at the left-top corner of the trigger element.
* LeftCenter :- The tooltip is positioned at the left-center of the trigger element.
* LeftBottom :- The tooltip is positioned at the left-bottom corner of the trigger element.
* RightTop :- The tooltip is positioned at the right-top corner of the trigger element.
* RightCenter :- The tooltip is positioned at the right-center of the trigger element.
* RightBottom :- The tooltip is positioned at the right-bottom corner of the trigger element.
* ```
*/
export type Position = 'TopLeft' | 'TopCenter' | 'TopRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight' | 'LeftTop' | 'LeftCenter' | 'LeftBottom' | 'RightTop' | 'RightCenter' | 'RightBottom';
/**
* Applicable tip positions attached to the Tooltip.
* ```props
* Auto :- The tip pointer position is automatically calculated based on the available space.
* Start :- The tip pointer is positioned at the start of the tooltip.
* Middle :- The tip pointer is positioned at the middle of the tooltip.
* End :- The tip pointer is positioned at the end of the tooltip.
* ```
*/
export type TipPointerPosition = 'Auto' | 'Start' | 'Middle' | 'End';
/**
* Animation effects that are applicable for Tooltip.
* ```props
* FadeIn :- A fade-in animation effect where the tooltip gradually increases in opacity from 0 to full.
* FadeOut :- A fade-out animation effect where the tooltip gradually decreases in opacity from full to 0.
* FadeZoomIn :- A fade-in animation effect combined with a zoom-in effect.
* FadeZoomOut :- A fade-out animation effect combined with a zoom-out effect.
* FlipXDownIn :- A flip-down animation effect where the tooltip starts upside down and flips down to become fully visible.
* FlipXDownOut :- A flip-down animation effect where the tooltip starts fully visible and flips down to become invisible.
* FlipXUpIn :- A flip-up animation effect where the tooltip starts upside down and flips up to become fully visible.
* FlipXUpOut :- A flip-up animation effect where the tooltip starts fully visible and flips up to become invisible.
* FlipYLeftIn :- A flip-left animation effect where the tooltip starts from the right side and flips left to become fully visible.
* FlipYLeftOut :- A flip-left animation effect where the tooltip starts from the left side and flips left to become invisible.
* FlipYRightIn :- A flip-right animation effect where the tooltip starts from the left side and flips right to become fully visible.
* FlipYRightOut :- A flip-right animation effect where the tooltip starts from the right side and flips right to become invisible.
* ZoomIn :- zoom-in animation effect where the tooltip starts small and gradually grows in size to become fully visible.
* ZoomOut :- A zoom-out animation effect where the tooltip starts full size and gradually decreases in size to become invisible.
* None :- No animation effect, the tooltip simply appears or disappears without any animation.
* ```
*/
export type Effect = 'FadeIn' | 'FadeOut' | 'FadeZoomIn' | 'FadeZoomOut' | 'FlipXDownIn' | 'FlipXDownOut' | 'FlipXUpIn' | 'FlipXUpOut' | 'FlipYLeftIn' | 'FlipYLeftOut' | 'FlipYRightIn' | 'FlipYRightOut' | 'ZoomIn' | 'ZoomOut' | 'None';
const TOUCHEND_HIDE_DELAY: number = 1500;
const TAPHOLD_THRESHOLD: number = 500;
const SHOW_POINTER_TIP_GAP: number = 0;
const HIDE_POINTER_TIP_GAP: number = 8;
const MOUSE_TRAIL_GAP: number = 2;
const POINTER_ADJUST: number = 2;
const ROOT: string = 'e-tooltip';
const RTL: string = 'e-rtl';
const DEVICE: string = 'e-bigger';
const ICON: string = 'e-icons';
const CLOSE: string = 'e-tooltip-close';
const TOOLTIP_WRAP: string = 'e-tooltip-wrap';
const CONTENT: string = 'e-tip-content';
const ARROW_TIP: string = 'e-arrow-tip';
const ARROW_TIP_OUTER: string = 'e-arrow-tip-outer';
const ARROW_TIP_INNER: string = 'e-arrow-tip-inner';
const TIP_BOTTOM: string = 'e-tip-bottom';
const TIP_TOP: string = 'e-tip-top';
const TIP_LEFT: string = 'e-tip-left';
const TIP_RIGHT: string = 'e-tip-right';
const POPUP_ROOT: string = 'e-popup';
const POPUP_OPEN: string = 'e-popup-open';
const POPUP_CLOSE: string = 'e-popup-close';
const POPUP_LIB: string = 'e-lib';
const HIDE_POPUP: string = 'e-hidden';
const POPUP_CONTAINER: string = 'e-tooltip-popup-container';
/**
* Describes the element positions.
*/
interface ElementPosition extends OffsetPosition {
position: Position;
horizontal: string;
vertical: string;
}
/**
* Interface for Tooltip event arguments.
*/
export interface TooltipEventArgs extends BaseEventArgs {
/**
* It is used to denote the type of the triggered event.
*/
type: string;
/**
* It illustrates whether the current action needs to be prevented or not.
*/
cancel: boolean;
/**
* It is used to specify the current event object.
*/
event: Event;
/**
* It is used to denote the current target element where the Tooltip is to be displayed.
*/
target: HTMLElement;
/**
* It is used to denote the Tooltip element
*/
element: HTMLElement;
/**
* It is used to denote the Collided Tooltip position
*/
collidedPosition?: string;
/**
* If the event is triggered by interaction, it returns true. Otherwise, it returns false.
*/
isInteracted?: boolean;
}
/**
* Animation options that are common for both open and close actions of the Tooltip.
*/
export interface TooltipAnimationSettings {
/**
* It is used to apply the Animation effect on the Tooltip, during open and close actions.
*/
effect?: Effect;
/**
* It is used to denote the duration of the animation that is completed per animation cycle.
*/
duration?: number;
/**
* It is used to denote the delay value in milliseconds and indicating the waiting time before animation begins.
*/
delay?: number;
}
export class Animation extends ChildProperty<Animation> {
/**
* Animation settings to be applied on the Tooltip, while it is being shown over the target.
*/
@Property<TooltipAnimationSettings>({ effect: 'FadeIn', duration: 150, delay: 0 })
public open: TooltipAnimationSettings;
/**
* Animation settings to be applied on the Tooltip, when it is closed.
*/
@Property<TooltipAnimationSettings>({ effect: 'FadeOut', duration: 150, delay: 0 })
public close: TooltipAnimationSettings;
}
/**
* Represents the Tooltip component that displays a piece of information about the target element on mouse hover.
* ```html
* <div id="tooltip">Show Tooltip</div>
* ```
* ```typescript
* <script>
* var tooltipObj = new Tooltip({ content: 'Tooltip text' });
* tooltipObj.appendTo("#tooltip");
* </script>
* ```
*/
@NotifyPropertyChanges
export class Tooltip extends Component<HTMLElement> implements INotifyPropertyChanged {
// internal variables
private popupObj: Popup;
private tooltipEle: HTMLElement;
private ctrlId: string;
private tipClass: string;
private tooltipPositionX: string;
private tooltipPositionY: string;
private tooltipEventArgs: TooltipEventArgs;
private isHidden: boolean;
private isTooltipOpen: boolean;
private showTimer: number;
private hideTimer: number;
private tipWidth: number;
private touchModule: Touch;
private tipHeight: number;
private autoCloseTimer: number;
private mouseMoveEvent: MouseEvent & TouchEvent = null;
private mouseMoveTarget: HTMLElement = null;
private containerElement: HTMLElement = null;
private isBodyContainer: boolean = true;
private targetsList: Element[];
// Tooltip Options
/**
* It is used to set the width of Tooltip component which accepts both string and number values.
* When set to auto, the Tooltip width gets auto adjusted to display its content within the viewable screen.
*
* @default 'auto'
*/
@Property('auto')
public width: string | number;
/**
* It is used to set the height of Tooltip component which accepts both string and number values.
* When Tooltip content gets overflow due to height value then the scroll mode will be enabled.
* Refer the documentation [here](https://ej2.syncfusion.com/documentation/tooltip/setting-dimension/)
* to know more about this property with demo.
*
* @default 'auto'
*/
@Property('auto')
public height: string | number;
/**
* It is used to display the content of Tooltip which can be both string and HTML Elements.
* Refer the documentation [here](https://ej2.syncfusion.com/documentation/tooltip/content/)
* to know more about this property with demo.
*
* {% codeBlock src="tooltip/content-api/index.ts" %}{% endcodeBlock %}
*
* @aspType string
*/
@Property()
public content: string | HTMLElement | Function;
/**
* It is used to set the container element in which the Tooltip’s pop-up will be appended. It accepts value as both string and HTML Element.
* It's default value is `body`, in which the Tooltip’s pop-up will be appended.
*
*/
@Property('body')
public container: string | HTMLElement;
/**
* It is used to denote the target selector where the Tooltip need to be displayed.
* The target element is considered as parent container.
*
* {% codeBlock src="tooltip/target-api/index.ts" %}{% endcodeBlock %}
*/
@Property()
public target: string;
/**
* It is used to set the position of Tooltip element, with respect to Target element.
*
* {% codeBlock src="tooltip/position-api/index.ts" %}{% endcodeBlock %}
*
*/
@Property('TopCenter')
public position: Position;
/**
* It sets the space between the target and Tooltip element in X axis.
*
* {% codeBlock src="tooltip/offsetX-api/index.ts" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/offsetx/index.md" %}{% endcodeBlock %}
*
* @default 0
*/
@Property(0)
public offsetX: number;
/**
* It sets the space between the target and Tooltip element in Y axis.
*
* {% codeBlock src="tooltip/offsetX-api/index.ts" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/offsety/index.md" %}{% endcodeBlock %}
*
* @default 0
*/
@Property(0)
public offsetY: number;
/**
* It is used to show or hide the tip pointer of Tooltip.
*
* {% codeBlock src="tooltip/offsetX-api/index.ts" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/showtippointer/index.md" %}{% endcodeBlock %}
*
* @default true
*/
@Property(true)
public showTipPointer: boolean;
/**
* It enables or disables the parsing of HTML string content into HTML DOM elements for Tooltip.
* If the value of the property is set to false, the tooltip content will be displayed as HTML string instead of HTML DOM elements.
*
* @default true
*/
@Property(true)
public enableHtmlParse: boolean;
/**
* It is used to set the collision target element as page viewport (window) or Tooltip element, when using the target.
* If this property is enabled, tooltip will perform the collision calculation between the target elements
* and viewport(window) instead of Tooltip element.
*
* @default false
*/
@Property(false)
public windowCollision: boolean;
/**
* It is used to set the position of tip pointer on tooltip.
* When it sets to auto, the tip pointer auto adjusts within the space of target's length
* and does not point outside.
* Refer the documentation
* [here](https://ej2.syncfusion.com/documentation/tooltip/position.html?lang=typescript#tip-pointer-positioning)
* to know more about this property with demo.
*
* {% codeBlock src="tooltip/tippointerposition/index.md" %}{% endcodeBlock %}
*
* @default 'Auto'
*/
@Property('Auto')
public tipPointerPosition: TipPointerPosition;
/**
* It is used to determine the device mode to display the Tooltip content.
* If it is in desktop, it will show the Tooltip content when hovering on the target element.
* If it is in touch device, it will show the Tooltip content when tap and holding on the target element.
*
* {% codeBlock src="tooltip/openson/index.md" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/opensOn-api/index.ts" %}{% endcodeBlock %}
*
* @default 'Auto'
*/
@Property('Auto')
public opensOn: string;
/**
* It allows the Tooltip to follow the mouse pointer movement over the specified target element.
* Refer the documentation [here](https://ej2.syncfusion.com/documentation/tooltip/position/#mouse-trailing)
* to know more about this property with demo.
*
* {% codeBlock src="tooltip/mousetrail/index.md" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/offsetX-api/index.ts" %}{% endcodeBlock %}
*
* @default false
*/
@Property(false)
public mouseTrail: boolean;
/**
* It is used to display the Tooltip in an open state until closed by manually.
* Refer the documentation [here](https://ej2.syncfusion.com/documentation/tooltip/open-mode/#sticky-mode)
* to know more about this property with demo.
*
* {% codeBlock src="tooltip/issticky/index.md" %}{% endcodeBlock %}
*
* @default false
*/
@Property(false)
public isSticky: boolean;
/**
* We can set the same or different animation option to Tooltip while it is in open or close state.
* Refer the documentation [here](https://ej2.syncfusion.com/documentation/tooltip/animation/)
* to know more about this property with demo.
*
* {% codeBlock src="tooltip/animation/index.md" %}{% endcodeBlock %}
* {% codeBlock src="tooltip/animation-api/index.ts" %}{% endcodeBlock %}
*
* @default { open: { effect: 'FadeIn', duration: 150, delay: 0 }, close: { effect: 'FadeOut', duration: 150, delay: 0 } }
*/
@Complex<AnimationModel>({}, Animation)
public animation: AnimationModel;
/**
* It is used to open the Tooltip after the specified delay in milliseconds.
*
* @default 0
*/
@Property(0)
public openDelay: number;
/**
* It is used to close the Tooltip after a specified delay in milliseconds.
*
* @default 0
*/
@Property(0)
public closeDelay: number;
/**
* It is used to customize the Tooltip which accepts custom CSS class names that
* defines specific user-defined styles and themes to be applied on the Tooltip element.
*
* @default null
*/
@Property()
public cssClass: string;
/**
* Specifies whether to display or remove the untrusted HTML values in the Tooltip component.
* If 'enableHtmlSanitizer' set to true, the component will sanitize any suspected untrusted strings and scripts before rendering them.
*
* @default true
*/
@Property(true)
public enableHtmlSanitizer: boolean;
/**
* Allows additional HTML attributes such as tabindex, title, name, etc. to root element of the Tooltip popup, and
* accepts n number of attributes in a key-value pair format.
*
* {% codeBlock src='tooltip/htmlAttributes/index.md' %}{% endcodeBlock %}
*
* @default {}
*/
@Property('')
public htmlAttributes: { [key: string]: string };
/**
* We can trigger `beforeRender` event before the Tooltip and its contents are added to the DOM.
* When one of its arguments `cancel` is set to true, the Tooltip can be prevented from rendering on the page.
* This event is mainly used for the purpose of customizing the Tooltip before it shows up on the screen.
* For example, to load the AJAX content or to set new animation effects on the Tooltip, this event can be opted.
* Refer the documentation
* [here](https://ej2.syncfusion.com/documentation/tooltip/content/#dynamic-content-via-ajax)
* to know more about this property with demo.
*
* @event beforeRender
*/
@Event()
public beforeRender: EmitType<TooltipEventArgs>;
/**
* We can trigger `beforeOpen` event before the Tooltip is displayed over the target element.
* When one of its arguments `cancel` is set to true, the Tooltip display can be prevented.
* This event is mainly used for the purpose of refreshing the Tooltip positions dynamically or to
* set customized styles in it and so on.
*
* {% codeBlock src="tooltip/beforeOpen/index.md" %}{% endcodeBlock %}
*
* @event beforeOpen
*/
@Event()
public beforeOpen: EmitType<TooltipEventArgs>;
/**
* We can trigger `afterOpen` event after the Tooltip Component gets opened.
*
* {% codeBlock src="tooltip/afterOpen/index.md" %}{% endcodeBlock %}
*
* @event afterOpen
*/
@Event()
public afterOpen: EmitType<TooltipEventArgs>;
/**
* We can trigger `beforeClose` event before the Tooltip hides from the screen. If returned false, then the Tooltip is no more hidden.
*
* {% codeBlock src="tooltip/beforeClose/index.md" %}{% endcodeBlock %}
*
* @event beforeClose
*/
@Event()
public beforeClose: EmitType<TooltipEventArgs>;
/**
* We can trigger `afterClose` event when the Tooltip Component gets closed.
*
* {% codeBlock src="tooltip/afterClose/index.md" %}{% endcodeBlock %}
*
* @event afterClose
*/
@Event()
public afterClose: EmitType<TooltipEventArgs>;
/**
* We can trigger `beforeCollision` event for every collision fit calculation.
*
* {% codeBlock src="tooltip/beforeCollision/index.md" %}{% endcodeBlock %}
*
* @event beforeCollision
*/
@Event()
public beforeCollision: EmitType<TooltipEventArgs>;
/**
* We can trigger `created` event after the Tooltip component is created.
*
* @event created
*/
@Event()
public created: EmitType<Object>;
/**
* We can trigger `destroyed` event when the Tooltip component is destroyed.
*
* @event destroyed
*/
@Event()
public destroyed: EmitType<Object>;
private windowResizeBound: () => void;
private keyDownBound: () => void;
private touchEndBound: () => void;
private scrollWheelBound: () => void;
/**
* Constructor for creating the Tooltip Component
*
* @param {TooltipModel} options - specifies the options for the constructor
* @param {string| HTMLElement} element - specifies the element for the constructor
*
*/
constructor(options?: TooltipModel, element?: string | HTMLElement) {
super(options, <HTMLElement | string>element);
}
private initialize(): void {
this.formatPosition();
addClass([this.element], ROOT);
}
private formatPosition(): void {
if (!this.position) { return; }
if (this.position.indexOf('Top') === 0 || this.position.indexOf('Bottom') === 0) {
[this.tooltipPositionY, this.tooltipPositionX] = this.position.split(/(?=[A-Z])/);
} else {
[this.tooltipPositionX, this.tooltipPositionY] = this.position.split(/(?=[A-Z])/);
}
}
private renderArrow(): void {
this.setTipClass(this.position);
const tip: HTMLElement = this.createElement('div', { className: ARROW_TIP + ' ' + this.tipClass });
tip.appendChild(this.createElement('div', { className: ARROW_TIP_OUTER + ' ' + this.tipClass }));
tip.appendChild(this.createElement('div', { className: ARROW_TIP_INNER + ' ' + this.tipClass }));
this.tooltipEle.appendChild(tip);
}
private setTipClass(position: Position): void {
if (position.indexOf('Right') === 0) {
this.tipClass = TIP_LEFT;
} else if (position.indexOf('Bottom') === 0) {
this.tipClass = TIP_TOP;
} else if (position.indexOf('Left') === 0) {
this.tipClass = TIP_RIGHT;
} else {
this.tipClass = TIP_BOTTOM;
}
}
private renderPopup(target: HTMLElement): void {
const elePos: OffsetPosition = this.mouseTrail ? { top: 0, left: 0 } : this.getTooltipPosition(target);
this.tooltipEle.classList.remove(POPUP_LIB);
this.popupObj = new Popup(this.tooltipEle as HTMLElement, {
height: this.height,
width: this.width,
position: { X: elePos.left, Y: elePos.top },
enableRtl: this.enableRtl,
open: this.openPopupHandler.bind(this),
close: this.closePopupHandler.bind(this)
});
}
private getScalingFactor(target: HTMLElement): { [key: string]: number } {
if (!target) {
return { x: 1, y: 1 };
}
const scalingFactors: { [key: string]: number } = { x: 1, y: 1 };
const elementsWithTransform: HTMLElement | Element = target.closest('[style*="transform: scale"]');
if (elementsWithTransform && elementsWithTransform !== this.tooltipEle && elementsWithTransform.contains(this.tooltipEle)) {
const computedStyle: CSSStyleDeclaration = window.getComputedStyle(elementsWithTransform);
const transformValue: string = computedStyle.getPropertyValue('transform');
const matrixValues: number[] = transformValue.match(/matrix\(([^)]+)\)/)[1].split(',').map(parseFloat);
scalingFactors.x = matrixValues[0];
scalingFactors.y = matrixValues[3];
}
return scalingFactors;
}
private getTooltipPosition(target: HTMLElement): OffsetPosition {
this.tooltipEle.style.display = 'block';
const parentWithZoomStyle: HTMLElement = this.element.closest('[style*="zoom"]') as HTMLElement;
if (parentWithZoomStyle) {
if (!parentWithZoomStyle.contains(this.tooltipEle)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.tooltipEle.style as any).zoom = (getComputedStyle(parentWithZoomStyle) as any).zoom;
}
}
const pos: OffsetPosition = calculatePosition(target, this.tooltipPositionX, this.tooltipPositionY, !this.isBodyContainer,
this.isBodyContainer ? null : this.containerElement.getBoundingClientRect());
const scalingFactors: { [key: string]: number } = this.getScalingFactor(target);
const offsetPos: OffsetPosition = this.calculateTooltipOffset(this.position, scalingFactors.x, scalingFactors.y);
const collisionPosition: Array<number> = this.calculateElementPosition(pos, offsetPos);
const collisionLeft: number = collisionPosition[0];
const collisionTop: number = collisionPosition[1];
const elePos: OffsetPosition = this.collisionFlipFit(target, collisionLeft, collisionTop);
elePos.left = elePos.left / scalingFactors.x;
elePos.top = elePos.top / scalingFactors.y;
this.tooltipEle.style.display = '';
return elePos;
}
private windowResize(): void {
this.reposition(this.findTarget());
}
private reposition(target: HTMLElement): void {
if (this.popupObj && target) {
const elePos: OffsetPosition = this.getTooltipPosition(target);
this.popupObj.position = { X: elePos.left, Y: elePos.top };
this.popupObj.dataBind();
}
}
private openPopupHandler(): void {
if (!this.mouseTrail && this.needTemplateReposition()) {
this.reposition(this.findTarget());
}
this.trigger('afterOpen', this.tooltipEventArgs);
this.tooltipEventArgs = null;
}
private closePopupHandler(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((this as any).isReact && !(this.opensOn === 'Click' || typeof (this.content) === 'function')) {
this.clearTemplate(['content']);
}
this.clear();
let tooltipAfterCloseEventArgs: TooltipEventArgs = {
type: this.tooltipEventArgs.event ? this.tooltipEventArgs.event.type : null, cancel: false,
target: this.tooltipEventArgs.target, event: this.tooltipEventArgs.event ? this.tooltipEventArgs.event : null,
element: this.tooltipEle, isInteracted: !isNullOrUndefined(this.tooltipEventArgs.event)
};
this.trigger('afterClose', tooltipAfterCloseEventArgs);
tooltipAfterCloseEventArgs = null;
}
private calculateTooltipOffset(position: Position, xScalingFactor: number = 1, yScalingFactor: number = 1): OffsetPosition {
const pos: OffsetPosition = { top: 0, left: 0 };
let tipWidth: number; let tipHeight: number; let tooltipEleWidth: number; let tooltipEleHeight: number; let arrowEle: HTMLElement;
let tipAdjust: number; let tipHeightAdjust: number; let tipWidthAdjust: number;
if (xScalingFactor !== 1 || yScalingFactor !== 1) {
const tooltipEleRect: DOMRect | ClientRect = this.tooltipEle.getBoundingClientRect();
let arrowEleRect: DOMRect | ClientRect;
tooltipEleWidth = Math.round(tooltipEleRect.width);
tooltipEleHeight = Math.round(tooltipEleRect.height);
arrowEle = select('.' + ARROW_TIP, this.tooltipEle) as HTMLElement;
if (arrowEle) { arrowEleRect = arrowEle.getBoundingClientRect(); }
tipWidth = arrowEle ? Math.round(arrowEleRect.width) : 0;
tipHeight = arrowEle ? Math.round(arrowEleRect.height) : 0;
tipAdjust = (this.showTipPointer ? SHOW_POINTER_TIP_GAP : HIDE_POINTER_TIP_GAP);
tipHeightAdjust = (tipHeight / 2) + POINTER_ADJUST + (tooltipEleHeight - (this.tooltipEle.clientHeight * yScalingFactor));
tipWidthAdjust = (tipWidth / 2) + POINTER_ADJUST + (tooltipEleWidth - (this.tooltipEle.clientWidth * xScalingFactor));
} else {
tooltipEleWidth = this.tooltipEle.offsetWidth;
tooltipEleHeight = this.tooltipEle.offsetHeight;
arrowEle = select('.' + ARROW_TIP, this.tooltipEle) as HTMLElement;
tipWidth = arrowEle ? arrowEle.offsetWidth : 0;
tipHeight = arrowEle ? arrowEle.offsetHeight : 0;
tipAdjust = (this.showTipPointer ? SHOW_POINTER_TIP_GAP : HIDE_POINTER_TIP_GAP);
tipHeightAdjust = (tipHeight / 2) + POINTER_ADJUST + (this.tooltipEle.offsetHeight - this.tooltipEle.clientHeight);
tipWidthAdjust = (tipWidth / 2) + POINTER_ADJUST + (this.tooltipEle.offsetWidth - this.tooltipEle.clientWidth);
}
if (this.mouseTrail) {
tipAdjust += MOUSE_TRAIL_GAP;
}
switch (position) {
case 'RightTop':
pos.left += tipWidth + tipAdjust;
pos.top -= tooltipEleHeight - tipHeightAdjust;
break;
case 'RightCenter':
pos.left += tipWidth + tipAdjust;
pos.top -= (tooltipEleHeight / 2);
break;
case 'RightBottom':
pos.left += tipWidth + tipAdjust;
pos.top -= (tipHeightAdjust);
break;
case 'BottomRight':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tipWidthAdjust);
break;
case 'BottomCenter':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth / 2);
break;
case 'BottomLeft':
pos.top += (tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth - tipWidthAdjust);
break;
case 'LeftBottom':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tipHeightAdjust);
break;
case 'LeftCenter':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tooltipEleHeight / 2);
break;
case 'LeftTop':
pos.left -= (tipWidth + tooltipEleWidth + tipAdjust);
pos.top -= (tooltipEleHeight - tipHeightAdjust);
break;
case 'TopLeft':
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth - tipWidthAdjust);
break;
case 'TopRight':
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tipWidthAdjust);
break;
default:
pos.top -= (tooltipEleHeight + tipHeight + tipAdjust);
pos.left -= (tooltipEleWidth / 2);
break;
}
pos.left += this.offsetX;
pos.top += this.offsetY;
return pos;
}
private updateTipPosition(position: Position): void {
const selEle: Element[] = selectAll('.' + ARROW_TIP + ',.' + ARROW_TIP_OUTER + ',.' + ARROW_TIP_INNER, this.tooltipEle);
const removeList: string[] = [TIP_BOTTOM, TIP_TOP, TIP_LEFT, TIP_RIGHT];
removeClass(selEle, removeList);
this.setTipClass(position);
addClass(selEle, this.tipClass);
}
private adjustArrow(target: HTMLElement, position: Position, tooltipPositionX: string, tooltipPositionY: string): void {
const arrowEle: HTMLElement = select('.' + ARROW_TIP, this.tooltipEle) as HTMLElement;
if (this.showTipPointer === false || arrowEle === null) { return; }
this.updateTipPosition(position);
let leftValue: string; let topValue: string;
this.tooltipEle.style.display = 'block';
const tooltipWidth: number = this.tooltipEle.clientWidth;
const tooltipHeight: number = this.tooltipEle.clientHeight;
const arrowInnerELe: HTMLElement = select('.' + ARROW_TIP_INNER, this.tooltipEle) as HTMLElement;
const tipWidth: number = arrowEle.offsetWidth; const tipHeight: number = arrowEle.offsetHeight;
this.tooltipEle.style.display = '';
if (this.tipClass === TIP_BOTTOM || this.tipClass === TIP_TOP) {
if (this.tipClass === TIP_BOTTOM) {
topValue = '99.9%';
// Arrow icon aligned -2px height from ArrowOuterTip div
arrowInnerELe.style.top = '-' + (tipHeight - 2) + 'px';
} else {
topValue = -(tipHeight - 1) + 'px';
// Arrow icon aligned -6px height from ArrowOuterTip div
arrowInnerELe.style.top = '-' + (tipHeight - 6) + 'px';
}
if (target) {
const tipPosExclude: boolean = tooltipPositionX !== 'Center' || (tooltipWidth > target.offsetWidth) || this.mouseTrail;
if ((tipPosExclude && tooltipPositionX === 'Left') || (!tipPosExclude && this.tipPointerPosition === 'End')) {
leftValue = (tooltipWidth - tipWidth - POINTER_ADJUST) + 'px';
} else if ((tipPosExclude && tooltipPositionX === 'Right') || (!tipPosExclude && this.tipPointerPosition === 'Start')) {
leftValue = POINTER_ADJUST + 'px';
} else if ((tipPosExclude) && (this.tipPointerPosition === 'End' || this.tipPointerPosition === 'Start')) {
leftValue = (this.tipPointerPosition === 'End') ? ((target.offsetWidth + ((this.tooltipEle.offsetWidth - target.offsetWidth) / 2)) - (tipWidth / 2)) - POINTER_ADJUST + 'px'
: ((this.tooltipEle.offsetWidth - target.offsetWidth) / 2) - (tipWidth / 2) + POINTER_ADJUST + 'px';
} else {
leftValue = ((tooltipWidth / 2) - (tipWidth / 2)) + 'px';
}
}
} else {
if (this.tipClass === TIP_RIGHT) {
leftValue = '99.9%';
// Arrow icon aligned -2px left from ArrowOuterTip div
arrowInnerELe.style.left = '-' + (tipWidth - 2) + 'px';
} else {
leftValue = -(tipWidth - 1) + 'px';
// Arrow icon aligned -2px from ArrowOuterTip width
arrowInnerELe.style.left = (-(tipWidth) + (tipWidth - 2)) + 'px';
}
const tipPosExclude: boolean = tooltipPositionY !== 'Center' || (tooltipHeight > target.offsetHeight) || this.mouseTrail;
if ((tipPosExclude && tooltipPositionY === 'Top') || (!tipPosExclude && this.tipPointerPosition === 'End')) {
topValue = (tooltipHeight - tipHeight - POINTER_ADJUST) + 'px';
} else if ((tipPosExclude && tooltipPositionY === 'Bottom') || (!tipPosExclude && this.tipPointerPosition === 'Start')) {
topValue = POINTER_ADJUST + 'px';
} else {
topValue = ((tooltipHeight / 2) - (tipHeight / 2)) + 'px';
}
}
arrowEle.style.top = topValue;
arrowEle.style.left = leftValue;
}
private renderContent(target?: HTMLElement): void {
const tooltipContent: HTMLElement = select('.' + CONTENT, this.tooltipEle) as HTMLElement;
if (this.cssClass) {
addClass([this.tooltipEle], this.cssClass.split(' '));
}
if (target && !isNullOrUndefined(target.getAttribute('title'))) {
target.setAttribute('data-content', target.getAttribute('title'));
target.removeAttribute('title');
}
if (!isNullOrUndefined(this.content)) {
tooltipContent.innerHTML = '';
if (this.content instanceof HTMLElement) {
tooltipContent.appendChild(this.content);
} else if (typeof this.content === 'string') {
if (this.isAngular) {
this.setProperties({ content: SanitizeHtmlHelper.sanitize(this.content) }, true);
}
else {
this.content = (this.enableHtmlSanitizer) ? SanitizeHtmlHelper.sanitize(this.content) : this.content;
}
if (this.enableHtmlParse) {
const tempFunction: Function = compile(this.content);
const tempArr: Element[] = tempFunction(
{}, this, 'content', this.element.id + 'content', undefined, undefined, tooltipContent, this.root);
if (tempArr) {
append(tempArr, tooltipContent);
}
} else {
tooltipContent['textContent'] = this.content;
}
} else {
const templateFunction: Function = compile(this.content);
const tempArr: Element[] = templateFunction(
{}, this, 'content', this.element.id + 'content', undefined, undefined, tooltipContent);
if (tempArr) {
if (this.isAngular) {
setTimeout(() => {
this.reposition(target);
}, 1);
}
append(tempArr, tooltipContent);
}
this.renderReactTemplates();
}
} else {
if (target && !isNullOrUndefined(target.getAttribute('data-content'))) {
tooltipContent.innerHTML = target.getAttribute('data-content');
}
}
}
private renderCloseIcon(): void {
if (!this.isSticky) {
const existingCloseIcon: HTMLElement = this.tooltipEle.querySelector('.' + ICON + '.' + CLOSE);
if (existingCloseIcon) {
remove(existingCloseIcon);
}
return;
}
const tipClose: HTMLElement = this.createElement('div', { className: ICON + ' ' + CLOSE, attrs: { role: 'button', 'aria-label': 'Press escape to close the Tooltip' } });
this.tooltipEle.appendChild(tipClose);
EventHandler.add(tipClose, Browser.touchStartEvent, this.onStickyClose, this);
}
private addDescribedBy(target: HTMLElement, id: string): void {
const describedby: string[] = (target.getAttribute('aria-describedby') || '').split(/\s+/);
if (describedby.indexOf(id) < 0) { describedby.push(id); }
attributes(target, { 'aria-describedby': describedby.join(' ').trim(), 'data-tooltip-id': id });
}
private removeDescribedBy(target: HTMLElement): void {
const id: string = target.getAttribute('data-tooltip-id');
const describedby: string[] = (target.getAttribute('aria-describedby') || '').split(/\s+/);
const index: number = describedby.indexOf(id);
if (index !== -1) {
describedby.splice(index, 1);
}
target.removeAttribute('data-tooltip-id');
const orgdescribedby: string = describedby.join(' ').trim();
if (orgdescribedby) {
target.setAttribute('aria-describedby', orgdescribedby);
} else {
target.removeAttribute('aria-describedby');
}
}
private tapHoldHandler(evt: TapEventArgs): void {
clearTimeout(this.autoCloseTimer);
this.targetHover(evt.originalEvent as Event);
}
private touchEndHandler(): void {
if (this.isSticky) {
return;
}
const close: Function = (): void => {
this.close();
};
this.autoCloseTimer = setTimeout(close, TOUCHEND_HIDE_DELAY);
}
private targetClick(e: Event): void {
let target: HTMLElement;
if (this.target) {
target = closest(e.target as HTMLElement, this.target) as HTMLElement;
} else {
target = this.element;
}
if (isNullOrUndefined(target)) {
return;
}
const mouseEvent: MouseEvent = e as MouseEvent;
if (target.getAttribute('data-tooltip-id') === null) {
if (!(mouseEvent.type === 'mousedown' && mouseEvent.button === 2)) {
this.targetHover(e);
}
} else if (!this.isSticky) {
this.hideTooltip(this.animation.close, e, target);
}
}
private targetHover(e: Event): void {
let target: HTMLElement;
if (this.target) {
target = closest(e.target as HTMLElement, this.target) as HTMLElement;
} else {
target = this.element;
}
if (isNullOrUndefined(target) || (target.getAttribute('data-tooltip-id') !== null && this.closeDelay === 0)) { return; }
if (!isNullOrUndefined(this.tooltipEle) && this.tooltipEle.getAttribute('e-animation-id')) {
PopupAnimation.stop(this.tooltipEle);
this.clear();
}
const targetList: Element[] = [].slice.call(selectAll('[data-tooltip-id= "' + this.ctrlId + '_content"]', document));
for (const target of targetList) {
this.restoreElement(target as HTMLElement);
}
this.showTooltip(target, this.animation.open, e);
}
private mouseMoveBeforeOpen(e: MouseEvent & TouchEvent): void {
this.mouseMoveEvent = e;
}
private mouseMoveBeforeRemove(): void {
if (this.mouseMoveTarget) {
EventHandler.remove(this.mouseMoveTarget, 'mousemove touchstart', this.mouseMoveBeforeOpen);
}
}
private showTooltip(target: HTMLElement, showAnimation: TooltipAnimationSettings, e?: Event): void {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
if (this.openDelay && this.mouseTrail) {
this.mouseMoveBeforeRemove();
this.mouseMoveTarget = target;
EventHandler.add(this.mouseMoveTarget, 'mousemove touchstart', this.mouseMoveBeforeOpen, this);
}
this.tooltipEventArgs = {
type: e ? e.type : null, cancel: false, target: target, event: e ? e : null,
element: this.tooltipEle, isInteracted: !isNullOrUndefined(e)
};
const observeCallback: Function = (beforeRenderArgs: TooltipEventArgs) => {
this.beforeRenderCallback(beforeRenderArgs, target, e, showAnimation);
};
this.trigger('beforeRender', this.tooltipEventArgs, observeCallback.bind(this));
}
private beforeRenderCallback(
beforeRenderArgs: TooltipEventArgs,
target: HTMLElement,
e: Event,
showAnimation: TooltipAnimationSettings): void {
if (beforeRenderArgs.cancel) {
this.isHidden = true;
this.clear();
this.mouseMoveBeforeRemove();
} else {
this.isHidden = false;
if (isNullOrUndefined(this.tooltipEle)) {
this.ctrlId = this.element.getAttribute('id') ?
getUniqueID(this.element.getAttribute('id')) : getUniqueID('tooltip');
this.tooltipEle = this.createElement('div', {
className: TOOLTIP_WRAP + ' ' + POPUP_ROOT + ' ' + POPUP_LIB, attrs: {
role: 'tooltip', 'aria-hidden': 'false', 'id': this.ctrlId + '_content'
}
});
this.tooltipEle.style.width = formatUnit(this.width);
this.tooltipEle.style.height = formatUnit(this.height);
this.tooltipEle.style.position = 'absolute';
this.tooltipBeforeRender(target, this);
this.tooltipAfterRender(target, e, showAnimation, this);
} else {
if (target) {
this.adjustArrow(target, this.position, this.tooltipPositionX, this.tooltipPositionY);
this.addDescribedBy(target, this.ctrlId + '_content');
this.renderContent(target);
PopupAnimation.stop(this.tooltipEle);
this.reposition(target);
this.tooltipAfterRender(target, e, showAnimation, this);
}
}
}
}
private appendContainer(ctrlObj: Tooltip): void {
if (typeof this.container == 'string') {
if (this.container === 'body') {
this.containerElement = document.body;
}
else {
this.isBodyContainer = false;
this.containerElement = select(this.container, document) as HTMLElement;
}
} else if (this.container instanceof HTMLElement) {
this.containerElement = this.container;
this.isBodyContainer = this.containerElement.tagName === 'BODY';
}
if (!this.isBodyContainer) {
addClass([this.containerElement], POPUP_CONTAINER);
}
this.containerElement.appendChild(ctrlObj.tooltipEle);
}
private tooltipBeforeRender(target: HTMLElement, ctrlObj: Tooltip): void {
if (target) {
if (Browser.isDevice) {
addClass([ctrlObj.tooltipEle], DEVICE);
}
if (ctrlObj.width !== 'auto') {
ctrlObj.tooltipEle.style.maxWidth = formatUnit(ctrlObj.width);
}
ctrlObj.tooltipEle.appendChild(ctrlObj.createElement('div', { className: CONTENT }));
this.appendContainer(ctrlObj);
removeClass([ctrlObj.tooltipEle], HIDE_POPUP);
ctrlObj.addDescribedBy(target, ctrlObj.ctrlId + '_content');
ctrlObj.renderContent(target);
addClass([ctrlObj.tooltipEle], POPUP_OPEN);
if (ctrlObj.showTipPointer) {
ctrlObj.renderArrow();
}
ctrlObj.renderCloseIcon();
ctrlObj.renderPopup(target);
ctrlObj.adjustArrow(target, ctrlObj.position, ctrlObj.tooltipPositionX, ctrlObj.tooltipPositionY);
PopupAnimation.stop(ctrlObj.tooltipEle);
ctrlObj.reposition(target);
}
}
private tooltipAfterRender(target: HTMLElement, e: Event, showAnimation: TooltipAnimationSettings, ctrlObj: Tooltip): void {
if (target) {
removeClass([ctrlObj.tooltipEle], POPUP_OPEN);
addClass([ctrlObj.tooltipEle], POPUP_CLOSE);
ctrlObj.tooltipEventArgs = {
type: e ? e.type : null, cancel: false, target: target, event: e ? e : null,
element: ctrlObj.tooltipEle, isInteracted: !isNullOrUndefined(e)
};
if (ctrlObj.needTemplateReposition() && !ctrlObj.mouseTrail && (showAnimation.effect === 'None' || showAnimation.effect === 'FadeIn' ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((this as any).isReact && typeof ctrlObj.content != 'string'))) {
ctrlObj.tooltipEle.style.display = 'none';
}
const observeCallback: Function = (observedArgs: TooltipEventArgs) => {
ctrlObj.beforeOpenCallback(observedArgs, target, showAnimation, e);
};
ctrlObj.trigger('beforeOpen', ctrlObj.tooltipEventArgs, observeCallback.bind(ctrlObj));
}
}
private beforeOpenCallback(
observedArgs: TooltipEventArgs,
target: HTMLElement,
showAnimation: TooltipAnimationSettings,
e: Event): void {
if (observedArgs.cancel) {
this.isHidden = true;
this.clear();
this.mouseMoveBeforeRemove();
this.restoreElement(target);
} else {
let openAnimation: Object = {
name: (showAnimation.effect === 'None' && animationMode === 'Enable') ? 'FadeIn' : this.animation.open.effect,
duration: showAnimation.duration,
delay: showAnimation.delay,
timingFunction: 'easeOut'
};
if (showAnimation.effect === 'None') {
openAnimation = undefined;
}
if (this.openDelay > 0) {
const show: Function = (): void => {
if (this.mouseTrail) {
EventHandler.add(target, 'mousemove touchstart mouseenter', this.onMouseMove, this);
}
if (this.popupObj) {
this.popupObj.show(openAnimation, target);
if (this.mouseMoveEvent && this.mouseTrail) { this.onMouseMove(this.mouseMoveEvent); }
}
};
this.showTimer = setTimeout(show, this.openDelay);
} else {
if (this.popupObj) { this.popupObj.show(openAnimation, target); }
}
}
if (e) {
this.wireMouseEvents(e, target);
}
}
private needTemplateReposition(): boolean {
// eslint-disable-next-line
const tooltip: any = this;
return !isNullOrUndefined(tooltip.viewContainerRef)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
&& typeof tooltip.viewContainerRef !== 'string' || (this as any).isReact;
}
private checkCollision(target: HTMLElement, x: number, y: number): ElementPosition {
const elePos: ElementPosition = {
left: x, top: y, position: this.position,
horizontal: this.tooltipPositionX, vertical: this.tooltipPositionY
};
const affectedPos: string[] = isCollide(this.tooltipEle, this.checkCollideTarget(), x, y);
if (affectedPos.length > 0) {
elePos.horizontal = affectedPos.indexOf('left') >= 0 ? 'Right' : affectedPos.indexOf('right') >= 0 ? 'Left' :
this.tooltipPositionX;
elePos.vertical = affectedPos.indexOf('top') >= 0 ? 'Bottom' : affectedPos.indexOf('bottom') >= 0 ? 'Top' :
this.tooltipPositionY;
}
return elePos;
}
private calculateElementPosition(pos: OffsetPosition, offsetPos: OffsetPosition): Array<number> {
return [this.isBodyContainer ? pos.left + offsetPos.left :
(pos.left - (this.containerElement.getBoundingClientRect()as DOMRect).left) +
offsetPos.left + window.pageXOffset + this.containerElement.scrollLeft,
this.isBodyContainer ? pos.top + offsetPos.top :