const TEMPLATE = document.createElement("template"); TEMPLATE.innerHTML = `
` export class BidiPanelElement extends HTMLElement { shadow mutationObserver #lastActive #currentIndex get contentContainer(){ return this.shadow.getElementById("content") } get progressBar(){ return this.shadow.getElementById("panel-progress-bar") } get progressDotsContainer(){ return this.shadow.getElementById("progress-dots") } get activeChildrenIndex(){ let thisGeometry = this.getBoundingClientRect() let progress = this.contentContainer.scrollLeft / (this.contentContainer.scrollWidth - thisGeometry.width) let focusedElementIndex = Math.floor(progress * this.children.length) if(Number.isFinite(focusedElementIndex)){ if(focusedElementIndex == this.children.length){ return this.children.length - 1 } else { return focusedElementIndex } } else { return null } } set activeChildrenIndex(newIndex){ this.setActiveChildrenIndex(newIndex, {behavior: "auto"}) } setActiveChildrenIndex(newIndex, options){ let newPanel = this.children[newIndex] let newPanelRect = newPanel.getBoundingClientRect() let selfRect = this.getBoundingClientRect() this.contentContainer.scrollTo({ left: this.contentContainer.scrollLeft + ( newPanelRect.left - selfRect.left ), behavior: options?.behavior || "auto" }) this.#currentIndex = newIndex } get activeChildren(){ return this.children[this.activeChildrenIndex] } connectedCallback(){ if(!this.shadow){ this.shadow = this.attachShadow({ mode: "open" }) this.shadow.append(TEMPLATE.content.cloneNode(true)) } this.contentContainer.addEventListener("scroll", this.handleContentScroll.bind(this)) this.shadow.getElementById("nav-left-panel").addEventListener("click", () => this.previous()); this.shadow.getElementById("nav-right-panel").addEventListener("click", () => this.next()); this.mutationObserver = (new MutationObserver(this.handleMutations.bind(this))) this.mutationObserver.observe(this, {childList:true}) this.updateProgress() } handleMutations(mutation){ let newIndex = this.#currentIndex this.updateProgress() if(Number.isFinite(newIndex)){ this.setActiveChildrenIndex(newIndex, {behavior: "instant"}) } this.requestDispatchChangeEvent() } handleContentScroll(e){ this.updateProgress() this.#currentIndex = this.activeChildrenIndex this.requestDispatchChangeEvent() } requestDispatchChangeEvent(){ let currentPanel = this.activeChildren if(currentPanel != this.#lastActive){ this.dispatchEvent(new ActivePanelChangeEvent("activePanelChange", this.#currentIndex || this.activeChildrenIndex)) this.#lastActive = currentPanel } } updateProgress(){ let focusedElementIndex = this.activeChildrenIndex; let progressBar = this.progressBar; progressBar.value = focusedElementIndex; progressBar.min = 0; progressBar.max = this.children.length-1; this.classList.toggle("single-panel", this.children.length == 1) this.classList.toggle("empty", this.children.length == 0) let dotsContainer = this.progressDotsContainer; if(dotsContainer.children.length != parseInt(progressBar.max)+1){ let dots = (new Array(this.children.length)) .fill(0) .map(() => { let el = document.createElement("div") el.classList.add("dot") el.part = "dot" return el }); dotsContainer.replaceChildren(...dots) } for(let inactiveDot of dotsContainer.querySelectorAll(".active")){ inactiveDot.part = "dot" inactiveDot.classList.remove("active") } let activeDot = dotsContainer.children[parseInt(progressBar.value)] if(activeDot){ activeDot.part = "dot-active" activeDot.classList.add("active") } } next(){ let nextItem = this.activeChildrenIndex + 1; if(nextItem < this.children.length){ this.activeChildrenIndex = nextItem } } previous(){ let nextItem = this.activeChildrenIndex - 1; if(nextItem >= 0){ this.activeChildrenIndex = nextItem } } } export class ActivePanelChangeEvent extends Event { constructor(name, activePanelIndex){ super(name) this.activePanelIndex = activePanelIndex } } customElements.define("camp-bidi-panel", BidiPanelElement);