Last active
May 21, 2024 22:10
-
-
Save elclanrs/bb719c17504e5d9b3ec985def050a041 to your computer and use it in GitHub Desktop.
VanillaJS popover with autoposition
This file contains hidden or 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
<button id="trigger" data-popover-target="my-popover">Popover</button> | |
<template data-popover="my-popover"> | |
This is the popover content! | |
</template> |
This file contains hidden or 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
function isInViewport(element) { | |
const rect = element.getBoundingClientRect(); | |
const html = document.documentElement; | |
return rect.top >= 0 && | |
rect.left >= 0 && | |
rect.bottom <= (window.innerHeight || html.clientHeight) && | |
rect.right <= (window.innerWidth || html.clientWidth); | |
} | |
class Popover { | |
constructor(trigger, { position = 'top', className = 'popover' }) { | |
this.trigger = trigger; | |
this.position = position; | |
this.className = className; | |
this.orderedPositions = ['top', 'right', 'bottom', 'left']; | |
const popoverTemplate = document.querySelector(`[data-popover=${trigger.dataset.popoverTarget}]`); | |
this.popover = document.createElement('div'); | |
this.popover.innerHTML = popoverTemplate.innerHTML; | |
Object.assign(this.popover.style, { | |
position: 'fixed' | |
}); | |
this.popover.classList.add(className); | |
this.handleWindowEvent = () => { | |
if (this.isVisible) { | |
this.show(); | |
} | |
}; | |
this.handleDocumentEvent = (evt) => { | |
if (this.isVisible && evt.target !== this.trigger && evt.target !== this.popover) { | |
this.popover.remove(); | |
} | |
}; | |
} | |
get isVisible() { | |
return document.body.contains(this.popover); | |
} | |
show() { | |
document.addEventListener('click', this.handleDocumentEvent); | |
window.addEventListener('scroll', this.handleWindowEvent); | |
window.addEventListener('resize', this.handleWindowEvent); | |
document.body.appendChild(this.popover); | |
const { top: triggerTop, left: triggerLeft } = this.trigger.getBoundingClientRect(); | |
const { offsetHeight: triggerHeight, offsetWidth: triggerWidth } = this.trigger; | |
const { offsetHeight: popoverHeight, offsetWidth: popoverWidth } = this.popover; | |
const positionIndex = this.orderedPositions.indexOf(this.position); | |
const positions = { | |
top: { | |
name: 'top', | |
top: triggerTop - popoverHeight, | |
left: triggerLeft - ((popoverWidth - triggerWidth) / 2) | |
}, | |
right: { | |
name: 'right', | |
top: triggerTop - ((popoverHeight - triggerHeight) / 2), | |
left: triggerLeft + triggerWidth | |
}, | |
bottom: { | |
name: 'bottom', | |
top: triggerTop + triggerHeight, | |
left: triggerLeft - ((popoverWidth - triggerWidth) / 2) | |
}, | |
left: { | |
name: 'left', | |
top: triggerTop - ((popoverHeight - triggerHeight) / 2), | |
left: triggerLeft - popoverWidth | |
} | |
}; | |
const position = this.orderedPositions | |
.slice(positionIndex) | |
.concat(this.orderedPositions.slice(0, positionIndex)) | |
.map(pos => positions[pos]) | |
.find(pos => { | |
this.popover.style.top = `${pos.top}px`; | |
this.popover.style.left = `${pos.left}px`; | |
return isInViewport(this.popover); | |
}); | |
this.orderedPositions.forEach(pos => { | |
this.popover.classList.remove(`${this.className}--${pos}`); | |
}); | |
if (position) { | |
this.popover.classList.add(`${this.className}--${position.name}`); | |
} else { | |
this.popover.style.top = positions.bottom.top; | |
this.popover.style.left = positions.bottom.left; | |
this.popover.classList.add(`${this.className}--bottom`); | |
} | |
} | |
hide() { | |
this.popover.remove(); | |
document.removeEventListener('click', this.handleDocumentEvent); | |
window.removeEventListener('scroll', this.handleWindowEvent); | |
window.removeEventListener('resize', this.handleWindowEvent); | |
} | |
toggle() { | |
if (this.isVisible) { | |
this.hide(); | |
} else { | |
this.show(); | |
} | |
} | |
} | |
const trigger = document.getElementById('trigger'); | |
const popover = new Popover(trigger, { position: 'top' }); | |
trigger.addEventListener('click', () => popover.toggle()); |
This file contains hidden or 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
body { | |
margin: 400px; | |
font: 16px/1.4 normal Arial, sans-serif; | |
} | |
@keyframes slide-top { | |
0% { | |
opacity: 0; | |
transform: translateY(-15%); | |
} | |
100% { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes slide-right { | |
0% { | |
opacity: 0; | |
transform: translateX(15%); | |
} | |
100% { | |
opacity: 1; | |
transform: translateX(0); | |
} | |
} | |
@keyframes slide-bottom { | |
0% { | |
opacity: 0; | |
transform: translateY(15%); | |
} | |
100% { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
@keyframes slide-left { | |
0% { | |
opacity: 0; | |
transform: translateX(-15%); | |
} | |
100% { | |
opacity: 1; | |
transform: translateX(0); | |
} | |
} | |
.popover { | |
$pad: 1.5em; | |
$bg-color: white; | |
$border-color: #aaa; | |
$arrow-pad: 8px; | |
$arrow-size: 8px; | |
$radius: 4px; | |
padding: $pad; | |
border: 1px solid $border-color; | |
border-radius: $radius; | |
background: $bg-color; | |
box-shadow: 0 1px 4px rgba(0,0,0,.2); | |
&--top { | |
margin-top: -$arrow-size - $arrow-pad; | |
animation: .4s slide-top; | |
&::before, &::after { | |
content: ""; | |
position: absolute; | |
top: 100%; | |
left: 50%; | |
margin-left: -$arrow-size; | |
border: $arrow-size solid transparent; | |
border-top-color: $bg-color; | |
} | |
&::before { | |
margin-top: 1px; | |
border-top-color: darken($border-color, 25%); | |
} | |
} | |
&--right { | |
margin-left: $arrow-size + $arrow-pad; | |
animation: .4s slide-right; | |
&::before, &::after { | |
content: ""; | |
position: absolute; | |
top: 50%; | |
right: 100%; | |
margin-top: -$arrow-size; | |
border: $arrow-size solid transparent; | |
border-right-color: $bg-color; | |
} | |
&::before { | |
margin-right: 1px; | |
border-right-color: darken($border-color, 25%); | |
} | |
} | |
&--bottom { | |
margin-top: $arrow-size + $arrow-pad; | |
animation: .4s slide-bottom; | |
&::before, &::after { | |
content: ""; | |
position: absolute; | |
bottom: 100%; | |
left: 50%; | |
margin-left: -$arrow-size; | |
border: $arrow-size solid transparent; | |
border-bottom-color: $bg-color; | |
} | |
&::before { | |
margin-bottom: 1px; | |
border-bottom-color: darken($border-color, 25%); | |
} | |
} | |
&--left { | |
margin-left: -$arrow-size - $arrow-pad; | |
animation: .4s slide-left; | |
&::before, &::after { | |
content: ""; | |
position: absolute; | |
top: 50%; | |
left: 100%; | |
margin-top: -$arrow-size; | |
border: $arrow-size solid transparent; | |
border-left-color: $bg-color; | |
} | |
&::before { | |
margin-left: 1px; | |
border-left-color: darken($border-color, 25%); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very nice pop-up, however, one correction a bit, on the popup style div please add display:inline
On line 22
Object.assign(this.popover.style, {
position: 'fixed',
display:'inline'
});