Skip to content

Instantly share code, notes, and snippets.

@Git-I985
Created May 5, 2023 13:41
Show Gist options
  • Save Git-I985/16328ac1a9a3b67f2a6b7999a2405a7f to your computer and use it in GitHub Desktop.
Save Git-I985/16328ac1a9a3b67f2a6b7999a2405a7f to your computer and use it in GitHub Desktop.
Popover feature TypeScript
export enum PopoverPositionType {
VERTICAL,
HORIZONTAL,
}
/**
* @param {Element|String} value
* @returns {Element}
*/
export const getHTMLElementFromArgument = (value): Element => {
if (value instanceof Element) {
return value;
}
if (typeof value === 'string') {
const elements = document.querySelectorAll(value);
if (elements.length === 1) {
return elements[0];
}
if (elements.length > 1) {
throw new Error(
`Found ${elements.length} elements with selector "${value}", which one did you want to use?`,
);
}
throw new Error(`Not found element with selector "${value}"`);
}
throw new Error(
`target element can only be a css selector or html element, ${Object.prototype.toString
.call(value)
.slice(8, -1)} passed`,
);
};
export class Popover {
public defaultStyles: Record<string, string> = {
display: 'block',
position: 'fixed',
'z-index': '9999',
visibility: 'hidden',
'pointer-events': 'none',
'backface-visibility': 'hidden',
transform: 'scale(.95)',
opacity: '.8',
transition: 'all .4s ease',
};
private popoverEl: HTMLElement;
private targetEl: HTMLElement;
private pageTopOffset: number;
private offset: number;
private showDelay: number;
private active: boolean;
private timeout: null | number;
private positionType: PopoverPositionType;
get activeStyles() {
return {
visibility: 'visible',
transform: 'none',
opacity: '1',
};
}
constructor(options: {
target: HTMLElement | string;
popover: HTMLElement | string;
pageTopOffset?: number;
targetOffset?: number;
showDelay?: number;
positionType?: PopoverPositionType;
}) {
const {
target,
popover,
pageTopOffset = 80,
targetOffset = 20,
showDelay = 200,
positionType = PopoverPositionType.HORIZONTAL,
} = options;
this.popoverEl = getHTMLElementFromArgument(popover).cloneNode(true) as HTMLElement;
this.targetEl = getHTMLElementFromArgument(target) as HTMLElement;
this.offset = targetOffset;
this.pageTopOffset = pageTopOffset;
this.showDelay = showDelay;
this.active = false;
this.timeout = null;
this.positionType = positionType;
this.targetEl.addEventListener('mouseenter', () => {
this.active = true;
this.timeout = setTimeout(this.show.bind(this), this.showDelay) as unknown as number;
});
this.targetEl.addEventListener('mouseleave', this.hide.bind(this));
document.addEventListener('scroll', this.hide.bind(this));
document.addEventListener('resize', this.hide.bind(this));
}
setStyles(stylesObj) {
Object.entries(stylesObj).forEach(([prop, val]) => (this.popoverEl.style[prop] = val));
}
getCoordinates(): { top: string; left: string } {
const targetRect = this.targetEl.getBoundingClientRect();
const targetYMiddle = targetRect.top + targetRect.height / 2;
const targetXMiddle = targetRect.left + targetRect.width / 2;
const popoverComputedStyle = window.getComputedStyle(this.popoverEl);
const popoverHeight = parseInt(popoverComputedStyle.getPropertyValue('height'));
const popoverWidth = parseInt(popoverComputedStyle.getPropertyValue('width'));
const viewportWidth = document.body.scrollWidth;
let popoverLeft = {
[PopoverPositionType.HORIZONTAL]: (() => {
let left = targetRect.right + this.offset;
let right = left + popoverWidth;
if (right > viewportWidth) {
left = targetRect.left - popoverWidth - this.offset;
}
if (left < 0) {
left = viewportWidth / 2 - popoverWidth / 2;
}
return left;
})(),
[PopoverPositionType.VERTICAL]: (() => {
if (popoverWidth > viewportWidth) {
return viewportWidth / 2 - popoverWidth / 2;
}
let left = targetXMiddle - popoverWidth / 2;
let right = left + popoverWidth;
let diff = right - viewportWidth;
return left < 0 ? 0 : left - Math.max(0, diff);
})(),
}[this.positionType];
let popoverTop = {
[PopoverPositionType.HORIZONTAL]: (() => {
let top = targetYMiddle - popoverHeight / 2;
let bottom = top + popoverHeight;
return top - Math.max(0, bottom - window.innerHeight);
})(),
[PopoverPositionType.VERTICAL]: (() => {
let top = targetRect.bottom + this.offset;
let bottom = top + popoverHeight;
return bottom > window.innerHeight ? targetRect.top - popoverHeight - this.offset : top;
})(),
}[this.positionType];
if (popoverTop < this.pageTopOffset) {
// dont overlap header
popoverTop = this.pageTopOffset;
}
return {
top: popoverTop + 'px',
left: popoverLeft + 'px',
};
}
show() {
document.body.append(this.popoverEl);
this.setStyles(this.defaultStyles);
this.setStyles({ ...this.getCoordinates(), ...this.activeStyles });
}
hide(): boolean | void {
if (!this.active) {
return false;
}
clearTimeout(this.timeout);
this.popoverEl.removeAttribute('style');
this.popoverEl.remove();
this.active = false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment