class Buildoz extends HTMLElement { constructor(){ super() // always call super() first! this.attrs = {} } static get observedAttributes(){ //observable attributes triggering attributeChangedCallback // anything added here will be observed for all buildoz tags // in your child, add you local 'color' observable attr with : // return([...super.observedAttributes, 'color']) return(['name']) } static define(name, cls){ const tag = `bz-${name}` if(!customElements.get(tag)) { // no wild redefinition customElements.define(tag, cls) } return cls } connectedCallback(){ // added to the DOM this.classList.add('buildoz') } disconnectedCallback(){ // removed from the DOM } attributeChangedCallback(name, oldValue, newValue) { this.attrs[name] = newValue if(name=='name') this.name = newValue } getBZAttribute(attrName){ // Little helper for defaults return(this.getAttribute(attrName) || this.defaultAttrs[attrName] ) } } class BZselect extends Buildoz { #value #fillFromMarkup = true constructor(){ super() this.value = null this.open = false this.value = null this.generalClickEvent = null this.defaultAttrs = { label: 'Select...', } } connectedCallback() { super.connectedCallback() this.button = document.createElement('button') this.button.textContent = this.getBZAttribute('label') this.prepend(this.button) this.button.addEventListener('click', this.toggle.bind(this)) if(!this.optionscontainer) this.optionscontainer = document.createElement('div') this.optionscontainer.classList.add('options-container') this.append(this.optionscontainer) this.options = this.querySelectorAll('option') 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 opt.addEventListener('click', this.onClick.bind(this)) if(opt.getAttribute('selected') !== null) this.onOption(opt.value, true) } this.#fillFromMarkup = false } } // static get observedAttributes(){ // Only if you want actions on attr change // return([...super.observedAttributes, 'disabled']) // } // attributeChangedCallback(name, oldValue, newValue) { // super.attributeChangedCallback(name, oldValue, newValue) // } get value(){ return(this.#value) } set value(v) { this.#value = v if(this.options && this.options.length>0) { const opt = Array.from(this.options).find(opt => opt.value==v) if(v || (opt && opt.textContent)) this.button.textContent = opt.textContent else this.button.textContent = this.getBZAttribute('label') this.dispatchEvent(new Event('change', { bubbles: true, composed: false, cancelable: false })) } } toggle(){ for(const opt of this.options){ if(this.open) { opt.classList.remove('open') this.optionscontainer.classList.remove('open') } else { document.querySelectorAll('bz-select').forEach((sel) => { if((sel!==this) && sel.open) sel.toggle() }) opt.classList.add('open') this.optionscontainer.classList.add('open') } } this.open = !this.open } onClick(evt){ evt.stopPropagation() const opt = evt.target.closest('option') if(opt && opt.value) this.onOption(opt.value) } onOption(value, silent=false){ if(this.getAttribute('disabled') !== null) return this.value = value if(!silent) this.toggle() } addOption(value, markup){ // Caution: you cannot count on connectedCallback to have run already, because one might fill before adding to the DOM const opt = document.createElement('option') opt.setAttribute('value', value) opt.innerHTML = markup 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.#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.options.forEach(node => { node.remove() }) this.options = this.querySelectorAll('option') this.onOption('', true) // unselect last } for(const opt of opts) this.addOption(opt.value, opt.markup) } } Buildoz.define('select', BZselect) class BZtoggler extends Buildoz { #value constructor(){ super() this.open = false this.defaultAttrs = { labelLeft:'', labelRight:'', trueValue: 'yes', falseValue: 'no', classOn:'', classOff:'', tabindex:0, disabled:false } } connectedCallback(){ super.connectedCallback() this.labelRight = document.createElement('div') this.labelRight.classList.add('toggle-label-right') this.labelRight.innerHTML = this.getBZAttribute('labelRight') this.labelLeft = document.createElement('div') this.labelLeft.classList.add('toggle-label-left') this.labelLeft.innerHTML = this.getBZAttribute('labelLeft') this.switch = document.createElement('div') this.switch.classList.add('toggle-switch') this.toggleBar = document.createElement('span') this.toggleBar.classList.add('toggle-bar') this.thumb = document.createElement('span') this.thumb.classList.add('toggle-thumb') this.toggleBar.append(this.thumb) this.switch.append(this.toggleBar) this.appendChild(this.labelLeft) this.appendChild(this.switch) this.appendChild(this.labelRight) this.setAttribute('tabindex', this.getBZAttribute('tabindex')) this.switch.addEventListener('click', this.toggle.bind(this)) } turnOn() { if(this.getBZAttribute('classOff')) this.switch.classList.remove(this.getBZAttribute('classOff')) if(this.getBZAttribute('classOn')) this.switch.classList.add(this.getBZAttribute('classOn')) this.thumb.classList.add('turned-on') this.#value = this.getBZAttribute('trueValue') this.focus() } turnOff() { if(this.getBZAttribute('classOn')) this.switch.classList.remove(this.getBZAttribute('classOn')) if(this.getBZAttribute('classOff'))this.switch.classList.add(this.getBZAttribute('classOff')) this.thumb.classList.remove('turned-on') this.#value = this.getBZAttribute('falseValue') this.focus() } toggle(event) { if(event) { event.preventDefault(); event.stopPropagation(); } if(this.getBZAttribute('disabled')) return if(this.#value == this.getBZAttribute('trueValue')) this.turnOff() else this.turnOn() this.dispatchEvent(new Event('change', { bubbles: true, composed: false, cancelable: false })) } get value(){ return(this.#value) } set value(v) { this.#value = v if(this.defaultAttrs){ if(this.#value == this.getBZAttribute('trueValue')) this.turnOn() else this.turnOff() this.dispatchEvent(new Event('change', { bubbles: true, composed: false, cancelable: false })) } } } Buildoz.define('toggler', BZtoggler) class BZslidePane extends Buildoz { constructor(){ super() this.open = false this.defaultAttrs = { side: 'bottom' } this.dragMove = this.dragMove.bind(this) this.dragEnd = this.dragEnd.bind(this) // Fill with innerHTML or other DOM manip should not allow coating to be removed this._observer = new MutationObserver(muts => { this.coat() }) } connectedCallback(){ super.connectedCallback() this.coat() this._observer.observe(this, { childList: true }) // Do this last } disconnectedCallback() { super.disconnectedCallback() this._observer.disconnect() } coat(){ if(this.handle && this.querySelector(this.dispatchEvent.handle)) return this._observer.disconnect() if(this.querySelector(this.dispatchEvent.handle)) this.querySelector(this.dispatchEvent.handle).remove() this.handle = document.createElement('div') this.handle.classList.add('handle') this.prepend(this.handle) this.handle.addEventListener('pointerdown', this.dragStart.bind(this)) this._observer.observe(this, { childList: true }) } dragStart(evt){ evt.target.setPointerCapture(evt.pointerId) this.dragStartX = evt.clientX this.dragStartY = evt.clientY this.handle.addEventListener('pointermove', this.dragMove) this.handle.addEventListener('pointerup', this.dragEnd) } dragMove(evt){ const box = this.getBoundingClientRect() const parentBox = this.parentElement.getBoundingClientRect() let width, height switch(this.getAttribute('side')){ case 'top': height = (evt.clientY > box.top) ? (evt.clientY - box.top) : 0 if(height>(parentBox.height/2)) height = Math.floor(parentBox.height/2) this.style.height = height+'px' break case 'bottom': height = (evt.clientY < box.bottom) ? (box.bottom - evt.clientY) : 0 if(height>(parentBox.height/2)) height = Math.floor(parentBox.height/2) this.style.height = height+'px' break case 'left': width = (evt.clientX > box.left) ? (evt.clientX - box.left) : 0 if(width>(parentBox.width/2)) width = Math.floor(parentBox.width/2) this.style.width = width+'px' break case'right': width = (evt.clientX < box.right) ? (box.right - evt.clientX) : 0 if(width>(parentBox.width/2)) width = Math.floor(parentBox.width/2) this.style.width = width+'px' break } } dragEnd(evt){ evt.target.releasePointerCapture(evt.pointerId) this.handle.removeEventListener('pointermove', this.dragMove) this.handle.removeEventListener('pointerup', this.dragEnd) } } Buildoz.define('slidepane', BZslidePane)