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);