Files
buildoz/buildoz.js
2026-03-21 15:49:40 +00:00

354 lines
12 KiB
JavaScript

/**
* _ ___ Another
* / |/ (_)______ __ _____
* / / / __(_-</ // (_-<
* /_/|_/_/\__/___/\_, /___/
* /___/
* production !
*
* Licensed under the MIT License:
* This code is free to use and modify,
* as long as the copyright notice and license are kept.
*/
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] )
}
fireEvent(eventName, detail){
let myname = this.tagName.toLocaleLowerCase()
myname = myname.substring(myname.indexOf('-')+1)
this.dispatchEvent(new CustomEvent(`bz:${myname}:${eventName}`, {
detail,
bubbles: true,
composed: true,
}))
}
}
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)