import { Automata } from './automata';
import CSS from './jb-infinite-scroll.css';
import { renderHTML } from './render';
import { type Elements, StateChangeWaitingBehavior } from './types.js';
import "jb-loading";
export * from "./types.js";
export class JBInfiniteScrollWebComponent extends HTMLElement {
elements!: Elements;
#isLoading = false;
//the amount of space that we listen to for trigger refresh
#endPageGap = 0;
#isListEnded = false;
//used when user scroll and we dont want to capture multiple scroll at one and wait for prev scroll event to finish
#isWaitingForStateChange = false;
#disableCaptureScroll = false;
// if user set empty state from outside (change to manual mode)
#externalIsListEmpty: boolean | null = null;
#internals!: ElementInternals;
get #isListEmpty() {
if (this.#externalIsListEmpty !== null) {
return this.#externalIsListEmpty;
}
return null;
}
#stateChangeWaitingBehavior: StateChangeWaitingBehavior = StateChangeWaitingBehavior.forceWait;
#automata!: Automata;
constructor() {
super();
if (typeof this.attachInternals == "function") {
//some browser dont support attachInternals
this.#internals = this.attachInternals();
}
this.#initWebComponent();
}
#mapStateChangeWaitingBehavior(state: string) {
switch (state) {
case "FORCE_WAIT":
return StateChangeWaitingBehavior.forceWait;
case "NO_WAIT":
return StateChangeWaitingBehavior.noWait;
default:
return StateChangeWaitingBehavior.forceWait;
}
}
set stateChangeWaitingBehavior(value: string) {
this.#stateChangeWaitingBehavior = this.#mapStateChangeWaitingBehavior(value);
}
get stateChangeWaitingBehavior() {
return this.#stateChangeWaitingBehavior;
}
get disableCaptureScroll() {
return this.#disableCaptureScroll;
}
set disableCaptureScroll(value: boolean) {
this.#disableCaptureScroll = value;
this.#setIsWaitingForStatChange(false);
}
get isListEnded() {
return this.#isListEnded;
}
set isListEnded(value: boolean) {
this.#isListEnded = value;
this.#setIsWaitingForStatChange(false);
}
get isLoading() {
return this.#isLoading;
}
set isLoading(value: boolean) {
this.#isLoading = value;
this.#setIsWaitingForStatChange(false);
if (value) {
this.elements?.loading.classList.add('--show');
(this.#internals as any).states?.add("loading");
} else {
this.elements?.loading.classList.remove('--show');
(this.#internals as any).states?.delete("loading");
this.#updateListDisplayState();
this.#checkScrollHeight();
}
}
get isListEmpty() {
return this.#externalIsListEmpty??false;
}
set isListEmpty(value: boolean) {
this.#externalIsListEmpty = value;
if (value) {
(this.#internals as any).states?.add("empty");
} else {
(this.#internals as any).states?.delete("empty");
}
this.#setIsWaitingForStatChange(false);
this.#updateListDisplayState();
}
#updateListDisplayState() {
if (this.#isListEmpty) {
this.elements?.emptyListWrapper.classList.add('--show');
this.elements?.contentWrapper.classList.remove('--show');
} else {
this.elements?.contentWrapper.classList.add('--show');
this.elements?.emptyListWrapper.classList.remove('--show');
}
}
connectedCallback() {
// standard web component event that called when all of dom is bounded
this.callOnLoadEvent();
this.#initProp();
this.callOnInitEvent();
}
callOnLoadEvent() {
const event = new CustomEvent('load', { bubbles: true, composed: false });
this.dispatchEvent(event);
}
callOnInitEvent() {
const event = new CustomEvent('init', { bubbles: true, composed: false });
this.dispatchEvent(event);
}
#initWebComponent() {
const shadowRoot = this.attachShadow({
mode: 'open',
clonable:true,
serializable:true,
});
const html = `\n${renderHTML()}`;
const element = document.createElement('template');
element.innerHTML = html;
shadowRoot.appendChild(element.content.cloneNode(true));
this.elements = {
loading: shadowRoot.querySelector('.loading-wrapper')!,
componentWrapper: shadowRoot.querySelector('.Infinite-scroll-component')!,
contentWrapper: shadowRoot.querySelector('.content-wrapper')!,
loadingWrapper: shadowRoot.querySelector('.loading-wrapper')!,
emptyListWrapper: shadowRoot.querySelector('.empty-list-wrapper')!,
} as const;
this.isLoading = this.hasAttribute('is-loading') ? this.getAttribute('is-loading') === 'true' : this.#isLoading;
this.#automata = new Automata(
{
scrollToEnd: this.scrollToEnd.bind(this)
},
{
contentWrapper:this.elements.contentWrapper,
contentSlot: this.elements.contentWrapper.querySelector('slot')!
}
);
}
#registerEventListener() {
this.elements.contentWrapper.addEventListener('scroll', this.#onScroll.bind(this), { passive: true });
}
#initProp() {
this.#registerEventListener();
this.#checkScrollHeight();
}
#checkScrollHeight() {
//check if our container is bigger than our content we call scroll end event
const { scrollHeight, clientHeight } = this.elements.contentWrapper;
if (scrollHeight <= clientHeight && this.canCaptureScroll) {
this.#onScrollEnd();
}
}
scrollTo(options: ScrollToOptions): void;
scrollTo(x: number, y: number): void;
scrollTo(params: number | ScrollToOptions, y?: number) {
y ? this.elements.contentWrapper.scrollTo(params as any, y) : this.elements.contentWrapper.scrollTo(params as any);
}
scrollToEnd(options?: Omit) {
this.scrollTo({ ...options, top: this.elements.contentWrapper.scrollHeight });
}
#onScrollEnd() {
this.#setIsWaitingForStatChange(true);
const event = new CustomEvent('scrollEnd');
this.dispatchEvent(event);
}
#onScroll() {
const { scrollTop, scrollHeight, clientHeight } = this.elements.contentWrapper;
this.#dispatchOnScroll();
if (scrollTop + clientHeight + this.#endPageGap >= scrollHeight) {
if (this.canCaptureScroll) {
this.#onScrollEnd();
}
}
}
#dispatchOnScroll(){
const event = new Event("scroll",{bubbles:false,cancelable:false,composed:true});
this.dispatchEvent(event);
}
#setIsWaitingForStatChange(isWaitingStatus: boolean) {
if (isWaitingStatus) {
if (this.#stateChangeWaitingBehavior === StateChangeWaitingBehavior.forceWait) {
this.#isWaitingForStateChange = true;
} else if (this.#stateChangeWaitingBehavior === StateChangeWaitingBehavior.noWait) {
return;
}
}
this.#isWaitingForStateChange = false;
}
get canCaptureScroll() {
if (!(this.#isLoading || this.#isListEmpty || this.#isListEnded || this.#isWaitingForStateChange || this.#disableCaptureScroll)) {
return true;
}
return false;
}
static get observedAttributes() {
return ['is-loading', 'is-list-empty', 'is-list-ended', 'disable-capture-scroll', 'state-change-waiting-behavior','stick-to-bottom'];
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
// do something when an attribute has changed
this.#onAttributeChange(name, newValue);
}
#onAttributeChange(name: string, value: string) {
switch (name) {
case 'is-loading':
if (value === 'true' || value ==='') {
this.isLoading = true;
} else {
this.isLoading = false;
}
break;
case 'is-list-empty':
if (value === 'true' || value ==='' ) {
this.isListEmpty = true;
} else {
this.isListEmpty = false;
}
break;
case 'is-list-ended':
if (value === 'true' || value ==='') {
this.#isListEnded = true;
} else {
this.#isListEnded = false;
}
break;
case 'disable-capture-scroll':
if (value === 'true' || value ==='') {
this.#disableCaptureScroll = true;
} else {
this.#disableCaptureScroll = false;
}
break;
case 'state-change-waiting-behavior':
this.#stateChangeWaitingBehavior = this.#mapStateChangeWaitingBehavior(value);
break;
case `stick-to-bottom`:
if (value === 'true' || value ==='') {
this.#automata.stickToBottom = true;
} else {
this.#automata.stickToBottom = false;
}
break;
}
}
}
const myElementNotExists = !customElements.get('jb-infinite-scroll');
if (myElementNotExists) {
window.customElements.define('jb-infinite-scroll', JBInfiniteScrollWebComponent);
}