portal trick on bz-select options

This commit is contained in:
STEINNI
2026-06-05 15:16:01 +00:00
parent 7f4e13c5e0
commit 63f894f526
2 changed files with 124 additions and 18 deletions
+15 -3
View File
@@ -41,7 +41,17 @@ bz-select > div.options-container{
transition: max-height 0.4s ease; transition: max-height 0.4s ease;
} }
bz-select > div.options-container.open{ pointer-events: auto; max-height: 10em;} bz-select > div.options-container.open{ pointer-events: auto; max-height: 10em;}
bz-select option{ div.options-container.portaled{
pointer-events: none;
position: fixed;
z-index: 10000;
max-height: 0;
overflow: auto;
transition: max-height 0.4s ease;
}
div.options-container.portaled.open{ pointer-events: auto; max-height: 10em;}
bz-select option,
div.options-container option{
background-color: #DDD; background-color: #DDD;
border: 1px solid black; border: 1px solid black;
color: #000; color: #000;
@@ -57,12 +67,14 @@ bz-select option{
margin-top 0.3s ease, margin-top 0.3s ease,
opacity 0.3s ease; opacity 0.3s ease;
} }
bz-select option.open{ bz-select option.open,
div.options-container option.open{
margin: 0; margin: 0;
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
} }
bz-select option:hover{ bz-select option:hover,
div.options-container option:hover{
background-color: #44F; background-color: #44F;
color: #FFF; color: #FFF;
} }
+109 -15
View File
@@ -74,7 +74,7 @@ class BZselect extends Buildoz {
if(!this.optionscontainer) this.optionscontainer = document.createElement('div') if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.classList.add('options-container') this.optionscontainer.classList.add('options-container')
this.append(this.optionscontainer) this.append(this.optionscontainer)
this.options = this.querySelectorAll('option') this.syncOptions()
if(this.#fillFromMarkup){ //can only do it once and only if fillOptions was not already called !! if(this.#fillFromMarkup){ //can only do it once and only if fillOptions was not already called !!
for(const opt of this.options){ for(const opt of this.options){
this.optionscontainer.append(opt) // Will move is to the right parent this.optionscontainer.append(opt) // Will move is to the right parent
@@ -85,6 +85,15 @@ class BZselect extends Buildoz {
} }
} }
disconnectedCallback() {
if(this.open || this._closing) this.closeDropdown(true)
}
syncOptions() {
if(this.optionscontainer) this.options = this.optionscontainer.querySelectorAll('option')
else this.options = []
}
// static get observedAttributes(){ // Only if you want actions on attr change // static get observedAttributes(){ // Only if you want actions on attr change
// return([...super.observedAttributes, 'disabled']) // return([...super.observedAttributes, 'disabled'])
@@ -111,19 +120,104 @@ class BZselect extends Buildoz {
} }
toggle(){ toggle(){
for(const opt of this.options){ if(this.open) this.closeDropdown()
if(this.open) { else this.openDropdown()
opt.classList.remove('open') }
this.optionscontainer.classList.remove('open')
} else { openDropdown() {
document.querySelectorAll('bz-select').forEach((sel) => { document.querySelectorAll('bz-select').forEach((sel) => {
if((sel!==this) && sel.open) sel.toggle() if((sel!==this) && sel.open) sel.closeDropdown(true)
}) })
opt.classList.add('open') this.optionscontainer.classList.add('portaled')
document.body.appendChild(this.optionscontainer)
this.positionPortal()
this.bindPortalListeners()
this.open = true
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if(!this.open) return
this.optionscontainer.classList.add('open') this.optionscontainer.classList.add('open')
for(const opt of this.options) opt.classList.add('open')
})
})
}
closeDropdown(immediate = false) {
if(!this.open) return
this.unbindPortalListeners()
for(const opt of this.options) opt.classList.remove('open')
this.optionscontainer.classList.remove('open')
if(immediate) {
this.clearCloseTransition()
this.finishCloseDropdown()
return
}
this._closing = true
this._onTransitionEnd = (evt) => {
if(evt.target !== this.optionscontainer) return
if(evt.propertyName !== 'max-height') return
this.finishCloseDropdownAfterTransition()
}
this.optionscontainer.addEventListener('transitionend', this._onTransitionEnd)
this._closeTimer = setTimeout(() => this.finishCloseDropdownAfterTransition(), 450)
}
finishCloseDropdownAfterTransition() {
if(!this._closing) return
this.clearCloseTransition()
this.finishCloseDropdown()
}
clearCloseTransition() {
this._closing = false
if(this._closeTimer) clearTimeout(this._closeTimer)
this._closeTimer = null
if(this._onTransitionEnd) {
this.optionscontainer.removeEventListener('transitionend', this._onTransitionEnd)
this._onTransitionEnd = null
}
}
finishCloseDropdown() {
this.optionscontainer.classList.remove('portaled')
this.clearPortalStyles()
if(this.optionscontainer.parentElement !== this) this.append(this.optionscontainer)
this.open = false
}
positionPortal() {
const rect = this.button.getBoundingClientRect()
this.optionscontainer.style.top = `${rect.bottom}px`
this.optionscontainer.style.left = `${rect.left}px`
this.optionscontainer.style.width = `${rect.width}px`
}
clearPortalStyles() {
this.optionscontainer.style.top = ''
this.optionscontainer.style.left = ''
this.optionscontainer.style.width = ''
}
bindPortalListeners() {
this._repositionBound = this.positionPortal.bind(this)
window.addEventListener('scroll', this._repositionBound, true)
window.addEventListener('resize', this._repositionBound)
this._outsideClickBound = (evt) => {
if(this.open && !this.contains(evt.target) && !this.optionscontainer.contains(evt.target)) {
this.closeDropdown()
} }
} }
this.open = !this.open this._outsideClickTimer = setTimeout(() => {
document.addEventListener('click', this._outsideClickBound, true)
}, 0)
}
unbindPortalListeners() {
if(this._repositionBound) window.removeEventListener('scroll', this._repositionBound, true)
if(this._repositionBound) window.removeEventListener('resize', this._repositionBound)
if(this._outsideClickBound) document.removeEventListener('click', this._outsideClickBound, true)
if(this._outsideClickTimer) clearTimeout(this._outsideClickTimer)
this._outsideClickTimer = null
} }
onClick(evt){ onClick(evt){
@@ -135,7 +229,7 @@ class BZselect extends Buildoz {
onOption(value, silent=false){ onOption(value, silent=false){
if(this.getAttribute('disabled') !== null) return if(this.getAttribute('disabled') !== null) return
this.value = value this.value = value
if(!silent) this.toggle() if(!silent && this.open) this.closeDropdown()
} }
addOption(value, markup){ addOption(value, markup){
@@ -146,16 +240,16 @@ class BZselect extends Buildoz {
opt.addEventListener('click',this.onClick.bind(this)) opt.addEventListener('click',this.onClick.bind(this))
if(!this.optionscontainer) this.optionscontainer = document.createElement('div') if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
this.optionscontainer.append(opt) this.optionscontainer.append(opt)
this.options = this.querySelectorAll('option') this.syncOptions()
this.#fillFromMarkup = false this.#fillFromMarkup = false
} }
fillOptions(opts, erase = true){ fillOptions(opts, erase = true){
// Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM // Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM
if(erase){ if(erase){
this.options = this.querySelectorAll('option') this.syncOptions()
this.options.forEach(node => { node.remove() }) this.options.forEach(node => { node.remove() })
this.options = this.querySelectorAll('option') this.syncOptions()
this.onOption('', true) // unselect last this.onOption('', true) // unselect last
} }
for(const opt of opts) this.addOption(opt.value, opt.markup) for(const opt of opts) this.addOption(opt.value, opt.markup)