import RevealAttributes from "../../attribute-helper/RevealAttributes.js"; import { childrenIterator } from "../../core/childrenIterator.js"; import { ComponentHelper } from "../../core/ComponentHelper.js"; import Defer from "../../core/Defer.js"; import { IEditableAttribute } from "../../core/EditableAttributes.js"; import { isInsideHtmlEditor } from "../../core/HtmlEditor.js"; import ScrollHelper from "../../core/ScrollHelper.js"; import { StringHelper } from "../../core/StringHelper.js"; import TemplateControl from "../../core/TemplateControl.js"; import { XNode } from "../../core/XNode.js"; let partId = 1; const added = Symbol("added"); class CarouselSlider extends TemplateControl { static observedAttributes = ["animate", "duration", "disable-scroll-animation", "hide-indicators"]; current: HTMLElement; indicator: HTMLElement; canvas: HTMLElement; root: ShadowRoot; // visibleObserver: IntersectionObserver; disableObserver = false; timer: any; prepared = false; // lastRecords: IntersectionObserverEntry[]; // visibleObserver = new IntersectionObserver((records) => { // this.updateIndicator(records); // }, { root: this, rootMargin: "0px", threshold: 0 }); get editableAttributes(): IEditableAttribute[] { return [ { name: "animate", type: "boolean" }, { name: "disable-scroll-animation", type: "boolean"}, { name: "duration", type: "time", suggestions: [ { label: "3s", value: "3s" }, { label: "5s", value: "5s" }, { label: "7s", value: "7s" }, { label: "10s", value: "10s" }, ] }, { name: "hide-indicators", type: "boolean" }, ]; } async onFirstPrepare() { this.setupAnimation(); this.prepared = true; this.queueUpdate(); } setupAnimation() { if (isInsideHtmlEditor(this)) { return; } const { timer } = this; if (timer) { clearInterval(timer) } const duration = StringHelper.parseTime(this.getAttribute("duration"), 5000); if (!/yes|true/i.test(this.getAttribute("animate"))) { return; } this.timer = setInterval(() => this.moveRight(true), duration); } attributeChangedCallback(name, oldValue, newValue) { switch(name) { case "animate": case "duration": this.setupAnimation(); break; } } updateCurrent() { const canvas = this.canvas; const { scrollLeft, offsetWidth } = canvas const scrollRight = scrollLeft + offsetWidth; let max = null as HTMLElement; for(const e of childrenIterator(canvas)) { const left = e.offsetLeft; const right = left + e.offsetWidth; if (scrollLeft >= left && scrollRight <= right) { max = e; } } if (!max) { return; } const slot = max; const target = childrenIterator.find(this, (e) => e.assignedSlot === slot); if (this.current !== target) { this.setCurrent(target, false); } this.updateIndicator(); } updateIndicator() { const slot = this.current.assignedSlot; const itemIndex = slot.getAttribute("item-index"); const { children } = this.indicator; for (let index = 0; index < children.length; index++) { const element = children[index]; const ei = element.getAttribute("item-indicator-index"); if (itemIndex === ei) { element.setAttribute("part", "circle active"); } else { element.setAttribute("part", "circle"); } } } indicatorClick = (e: MouseEvent) => { // console.log(e); let { value } = ComponentHelper.getParentAttribute(e.target as HTMLElement, (x) => x.getAttribute("item-indicator-index")); if (!value) { return; } const slot = this.canvas.querySelector(`[item-index="${value}"]`) as HTMLSlotElement; this.current = slot.assignedElements()[0] as HTMLElement; ScrollHelper.bringIntoView(this.current, this.getAttribute("disable-scroll-animation") !== "true"); this.setupAnimation(); }; async prepare() { const root = this.attachShadow({ mode: "open", slotAssignment: "manual"}) this.root = root; XNode.render(root,
this.moveLeft() } part="left">
this.moveRight() } part="right">
); const indicator = root.querySelector(`[part="indicator"]`) as HTMLElement; this.indicator = indicator; const canvas = root.querySelector(`[part="canvas"]`) as HTMLElement; this.canvas = canvas; this.canvas.addEventListener("scroll", () => this.queueUpdate()); let disposableMouseMove: Disposable; let disposableMouseEnd: Disposable; let disposableMouseLeave: Disposable; const dispose = (me: MouseEvent) => { me.preventDefault(); me.stopImmediatePropagation(); disposableMouseMove?.[Symbol.dispose](); disposableMouseEnd?.[Symbol.dispose](); disposableMouseLeave?.[Symbol.dispose](); this.removeAttribute("scrolling"); disposableMouseEnd = null; disposableMouseMove = null; disposableMouseLeave = null; this.disableObserver = false; }; if (isInsideHtmlEditor(this)) { return; } this.canvas.addEventListener("dragstart", (e) => { e.preventDefault(); e.stopImmediatePropagation(); }); this.canvas.addEventListener("mousedown", (me: MouseEvent) => { me.preventDefault(); me.stopImmediatePropagation(); this.disableObserver = true; this.setAttribute("scrolling", "yes"); disposableMouseMove ??= this.bindEvent( this.canvas, "mousemove", (me: MouseEvent) => { me.preventDefault(); me.stopImmediatePropagation(); this.canvas.scrollBy({ left: -me.movementX, behavior: "instant"}); }); disposableMouseEnd ??= this.bindEvent( this.canvas, "mouseup", dispose); disposableMouseEnd ??= this.bindEvent( this.canvas, "mouseleave", dispose); }); this.queueUpdate(); this.events.add(document, "visibilitychange", (e) => { if(document.hidden) { clearInterval(this.timer); } else { this.setupAnimation(); } }); } onChildAdded(iterator: HTMLElement) { if (iterator[added]) { return; } iterator[added] = true; // this.visibleObserver.observe(iterator); // this.setAttribute("has-children", "true"); const slot = this.ownerDocument.createElement("slot"); const { canvas, indicator } = this; const i = partId++; const name = `child${i}`; slot.setAttribute("name", name); slot.setAttribute("part", "child"); slot.setAttribute("item-index", i as any as string); canvas.appendChild(slot); // iterator.slot = name; slot.assign(iterator); const span = this.ownerDocument.createElement("span"); span.setAttribute("part", "circle"); span.setAttribute("item-indicator-index", i as any as string); indicator.appendChild(span); span.addEventListener("click", () => { // this.current = iterator; // this.setupAnimation(); // ScrollHelper.bringIntoView(this.current, true); // this.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start"}); this.setCurrent(iterator, true); }); if (!this.current) { this.queueUpdate(); } } @Defer(100) queueUpdate() { this.updateCurrent(); } onChildRemoved(child: HTMLElement) { // this.visibleObserver.unobserve(child); const { indicator } = this; const { slot } = child as any; if (!slot) { return; } const childSlot = child.assignedSlot; const next = (childSlot.nextElementSibling ?? childSlot.previousElementSibling) as HTMLSlotElement; childSlot.remove(); const index = childSlot.getAttribute("item-index"); const span = indicator.querySelector(`[item-indicator-index="${index}"]`); span?.remove(); if (!this.children.length) { this.removeAttribute("has-children"); } if (next) { const nextChild = next.assignedElements()[0]; this.setCurrent(nextChild as HTMLElement); } else { this.setCurrent(null); } } moveLeft() { const previous = this.current?.previousElementSibling as HTMLElement ?? this.lastElementChild as HTMLElement; this.setCurrent(previous, true); } setCurrent(current: HTMLElement, setupAnimation = false) { const previous = this.current; if (this.current === current) { return; } this.current = current ?? this.firstElementChild as HTMLElement; if (setupAnimation) { this.setupAnimation(); } const isEditing = isInsideHtmlEditor(this); if (!isEditing) { RevealAttributes.addAll(current); } ScrollHelper.bringIntoView(this.current, this.getAttribute("disable-scroll-animation") !== "true"); setTimeout(() => { if (!isEditing && previous) { RevealAttributes.removeAll(previous); } }, 300); } moveRight(adjust = false) { const next = this.current?.nextElementSibling as HTMLElement ?? this.firstElementChild as HTMLElement; if (!next) { return; } this.setCurrent(next, true); if (adjust) { setTimeout(() => { const target = next.assignedSlot; const parent = target.parentElement; let { nextElementSibling, } = target; if(!nextElementSibling) { const { firstElementChild } = parent; const width = this.offsetWidth; const scrollLeft = this.canvas.scrollLeft - width; firstElementChild.remove(); parent.appendChild(firstElementChild); setTimeout(() => { this.canvas.scrollLeft = scrollLeft; },1); } }, 1000); } } makeChildVisible(e: HTMLElement) { this.setCurrent(e, false); } } customElements.define("carousel-slider", CarouselSlider);