Created
May 5, 2023 13:41
-
-
Save Git-I985/16328ac1a9a3b67f2a6b7999a2405a7f to your computer and use it in GitHub Desktop.
Popover feature TypeScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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