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;
|
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
@@ -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
|
||||||
@@ -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
|
// 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user