portal trick on bz-select options
This commit is contained in:
+15
-3
@@ -41,7 +41,17 @@ bz-select > div.options-container{
|
||||
transition: max-height 0.4s ease;
|
||||
}
|
||||
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;
|
||||
border: 1px solid black;
|
||||
color: #000;
|
||||
@@ -57,12 +67,14 @@ bz-select option{
|
||||
margin-top 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
bz-select option.open{
|
||||
bz-select option.open,
|
||||
div.options-container option.open{
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
bz-select option:hover{
|
||||
bz-select option:hover,
|
||||
div.options-container option:hover{
|
||||
background-color: #44F;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
+107
-13
@@ -74,7 +74,7 @@ class BZselect extends Buildoz {
|
||||
if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
|
||||
this.optionscontainer.classList.add('options-container')
|
||||
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 !!
|
||||
for(const opt of this.options){
|
||||
this.optionscontainer.append(opt) // Will move is to the right parent
|
||||
@@ -86,6 +86,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
|
||||
// return([...super.observedAttributes, 'disabled'])
|
||||
// }
|
||||
@@ -111,19 +120,104 @@ class BZselect extends Buildoz {
|
||||
}
|
||||
|
||||
toggle(){
|
||||
for(const opt of this.options){
|
||||
if(this.open) {
|
||||
opt.classList.remove('open')
|
||||
this.optionscontainer.classList.remove('open')
|
||||
} else {
|
||||
if(this.open) this.closeDropdown()
|
||||
else this.openDropdown()
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
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')
|
||||
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
|
||||
}
|
||||
}
|
||||
this.open = !this.open
|
||||
|
||||
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._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){
|
||||
@@ -135,7 +229,7 @@ class BZselect extends Buildoz {
|
||||
onOption(value, silent=false){
|
||||
if(this.getAttribute('disabled') !== null) return
|
||||
this.value = value
|
||||
if(!silent) this.toggle()
|
||||
if(!silent && this.open) this.closeDropdown()
|
||||
}
|
||||
|
||||
addOption(value, markup){
|
||||
@@ -146,16 +240,16 @@ class BZselect extends Buildoz {
|
||||
opt.addEventListener('click',this.onClick.bind(this))
|
||||
if(!this.optionscontainer) this.optionscontainer = document.createElement('div')
|
||||
this.optionscontainer.append(opt)
|
||||
this.options = this.querySelectorAll('option')
|
||||
this.syncOptions()
|
||||
this.#fillFromMarkup = false
|
||||
}
|
||||
|
||||
fillOptions(opts, erase = true){
|
||||
// Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM
|
||||
if(erase){
|
||||
this.options = this.querySelectorAll('option')
|
||||
this.syncOptions()
|
||||
this.options.forEach(node => { node.remove() })
|
||||
this.options = this.querySelectorAll('option')
|
||||
this.syncOptions()
|
||||
this.onOption('', true) // unselect last
|
||||
}
|
||||
for(const opt of opts) this.addOption(opt.value, opt.markup)
|
||||
|
||||
Reference in New Issue
Block a user