import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber";
import { map } from "lit/directives/map.js";
import DataProvider, { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
import "@supersoniks/concorde/core/components/functional/list/list";
import { HTML } from "@supersoniks/concorde/utils";
import { ListItems } from "@supersoniks/concorde/core/components/functional/list/list";
type QueueItem = {
id: string;
endPoint: string;
dataProvider: string;
offset: number;
limit: number;
};
type QueueProps = QueueItem[] & {
resultCount?: number;
lastFetchedData?: unknown;
};
const tagName = "sonic-queue";
/**
*### Une Queue charge du contenu par lot selon l'expression renseignée dans l'attribut *dataProviderExpression*.
* * Chaque lot est chargé par un composant [List](./?path=/docs/core-components-functional-list-list--basic) dont le dataProvider créé à partir de l'attribut dataProviderExpression
* * A l'initialisation elle regarde l'attribut dataFilterProvider qui donne l'adresse d'un publisher
* Si cet attribut est touvé, Queue écoute le publisher fourni et se réinitialise à chaque modification du contenu de celui-ci.
* Les valeurs renseignées dans ce publisher sont ajoutées en get à chaque requête
* * la proriété *key* peut être utilisé pour cibler une propriété particulière du retour de l'api.
*/
@customElement(tagName)
export default class Queue extends Subscriber(LitElement, {} as QueueProps) {
@property({ type: Array }) templates: Array | null =
null;
@property({ type: Function }) items: ListItems | null = null;
@property({ type: Function }) noItems: ListItems | null = null;
@property({ type: Function }) skeleton: ListItems | null = null;
lastRequestTime = 0;
key = "";
@property({ type: Object }) itemPropertyMap: object | null = null;
/**
* Durée cible en ms d'une requête pour afficher 1 lot.
*/
@property() cache: RequestCache = "default";
@property() targetRequestDuration = 500;
/*
* Quantité d'éléments devant être chargés dans le premier lot.
* Cette valeur est mise à jour ensuite par Queue pour chauq lot pour se rapprocher tanque possible de *targetRequestDuration*
*/
@property() limit = 5;
@property() lazyBoundsRatio = 1;
@property() offset = 0;
@property() resultCount = 0;
@property({ type: Boolean }) noLazyload = false;
@property({ type: String }) loader = "inline";
@property() filteredFields = "";
disconnectedCallback() {
for (const dataProvider of this.listDataProviders) {
PublisherManager.delete(dataProvider);
this.listDataProviders = [];
}
this.filterPublisher?.offInternalMutation(this.updateFilteredContent);
// reset internal state
this.props = null;
this.limit = 5;
this.offset = 0;
this.resultCount = 0;
this.searchHash = "";
this.requestId = 0;
this.isFirstRequest = true;
this.nextHadEvent = false;
this.publisher.set({});
super.disconnectedCallback();
return;
}
static instanceCounter = 0;
instanceId = 0;
localStorage = "disabled";
async connectedCallback() {
this.instanceId = Queue.instanceCounter++;
this.localStorage = this.getAttribute("localStorage") || this.localStorage;
this.filterTimeoutMs = parseInt(
this.getAttribute("filterTimeoutMs") || "400"
);
//On supprime l'attribut car une queue ne doi pas être en localstorage, ce sont ses sous composants list qui doivent l'être
this.removeAttribute("localStorage");
this.noShadowDom = "";
this.defferedDebug = this.hasAttribute("debug") || null;
/**Compat avec states et routing **/
if (!this.dataProvider)
this.dataProvider =
this.dataProviderExpression ||
"sonic-queue-" +
this.instanceId +
"-" +
Math.random().toString(36).substring(7);
if (!this.dataProviderExpression) {
this.dataProviderExpression =
HTML.getAncestorAttributeValue(this.parentElement, "dataProvider") ||
"";
}
this.storeScrollPosition();
super.connectedCallback();
this.publisher.set({});
this.key = this.getAttribute("key");
await PublisherManager.getInstance().isLocalStrorageReady;
if (!this.templates)
this.templates = Array.from(
this.querySelectorAll("template")
) as Array;
this.lastRequestTime = new Date().getTime();
this.configFilter();
}
filterPublisher: DataProvider | null = null;
configFilter() {
const dataFilterProvider =
this.getAncestorAttributeValue("dataFilterProvider");
if (!dataFilterProvider) {
this.next();
return;
}
this.filterPublisher =
PublisherManager.getInstance().get(dataFilterProvider);
this.filterPublisher?.onInternalMutation(this.updateFilteredContent);
}
filterTimeoutId?: ReturnType;
filterTimeoutMs = 400;
searchHash = "";
requestId = 0;
isFirstRequest = true;
updateFilteredContent = () => {
/**
* On ne lance la recherche que si le hash de recherche est différent
*/
const dataProvider = this.dataProviderExpression;
const split = dataProvider.split("?");
split.shift();
const searchParams = new URLSearchParams(split.join("?"));
const filterData: Record =
this.filterPublisher?.get() ;
const filteredFieldsArray = this.filteredFields.split(" ");
for (const f in filterData) {
let value = filterData[f];
if (Array.isArray(value))
value = value.filter((v: string | null) => v !== null);
if (
(this.filteredFields && !filteredFieldsArray.includes(f)) ||
value == null ||
value.toString() === ""
)
continue;
searchParams.set(f, filterData[f].toString());
}
const searchHash = searchParams.toString();
if (searchHash == this.searchHash && !this.isFirstRequest) return;
this.searchHash = searchHash;
/**
* on reset les données avant de lancer la requète
*/
for (const dataProvider of this.listDataProviders) {
PublisherManager.delete(dataProvider);
// this.publisher.lastFetchedData = {};
}
this.listDataProviders = [];
clearTimeout(this.filterTimeoutId);
this.filterTimeoutId = setTimeout(
async () => {
const count = this.resultCount;
this.props = null;
//On garde le décompte au cas ou il n'y aurait pas rechargement
this.requestId++;
this.resultCount = count;
await PublisherManager.getInstance().isLocalStrorageReady;
window.requestAnimationFrame(() => this.next());
},
this.isFirstRequest ? 0 : this.filterTimeoutMs
);
this.isFirstRequest = false;
};
storeScrollPosition() {
if (!this.isSafari()) {
return;
}
this.storedScrollX = window.scrollX;
this.storedScrollY = window.scrollY;
}
isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
protected updated(_changedProperties: PropertyValues): void {
if (this.isSafari()) {
if (
Math.abs(this.storedScrollX - window.scrollX) > 10 ||
Math.abs(this.storedScrollY - window.scrollY) > 10
) {
window.scrollTo(this.storedScrollX, this.storedScrollY);
}
//Hack pour éviter que le scroll ne retourne en haut si safari
window.requestAnimationFrame(() => {
if (
Math.abs(this.storedScrollX - window.scrollX) > 10 ||
Math.abs(this.storedScrollY - window.scrollY) > 10
) {
window.scrollTo(this.storedScrollX, this.storedScrollY);
}
});
}
super.updated(_changedProperties);
}
/**
* Cette expression est utilisée comme modèle par le composant Queue pour renseigngner le dataProvider de la [liste](./?path=/docs/core-components-functional-list-list--basic) créée.
* * l'expression *$offset* est alors remplacée par le numéro de l'élément à partir duquel démarrer
* * l'expression *$limit* est remplacée par la valeur représentant le nombre d'éléments à charger
* * Si pas d'expression *$offset* le composant se comporte un peu comme une liste, il ne va pas essayer de charger les éléments suivants
*/
@property({ type: String }) dataProviderExpression = "";
@property({ type: Boolean }) invalidateOnPageShow = false;
@property({ type: String }) idKey = "id";
resetDuration() {
this.lastRequestTime = new Date().getTime();
}
listDataProviders: string[] = [];
nextHadEvent = false;
next(e?: CustomEvent) {
let offset = this.offset;
const newTime = new Date().getTime();
const requestDuration = newTime - this.lastRequestTime;
/**
* Le rechargement n'est pas garanti si pas de changement dans les filtres
* Un ne repasse donc à 0 qu'à partir du premier chargement.
* */
if (!this.nextHadEvent && e) {
this.publisher.resultCount = 0;
// this.publisher.lastFetchedData = {};
this.resultCount = 0;
}
this.nextHadEvent = !!e;
if (e) {
this.publisher.lastFetchedData = e.detail.fetchedData;
if (e.detail.requestId < this.requestId) return;
this.resultCount += e.detail.props?.length || 0;
if (
!e.detail.isFirstLoad ||
!e.detail.props?.length ||
this.dataProviderExpression.indexOf("$offset") == -1
) {
this.publisher.resultCount = this.resultCount;
// this.publisher.lastFetchedData = {};
return;
}
}
if (!Array.isArray(this.props)) {
const newProps: QueueProps = [];
newProps.resultCount = this.resultCount;
newProps.lastFetchedData = e?.detail.fetchedData || {};
this.props = newProps;
} else {
const props: Array<{ offset: number; limit: number }> = this.props;
const item = props[props.length - 1];
offset =
parseInt(item.offset.toString()) + parseInt(item.limit.toString());
}
if (requestDuration > 0 && e && !this.localStorage) {
this.limit = Math.round(
(this.limit / requestDuration) * this.targetRequestDuration
);
}
if (this.limit < 1) this.limit = 1;
if (this.limit > 15) this.limit = 15;
let dataProvider = this.dataProviderExpression
.replace("$offset", offset + "")
.replace("$limit", this.limit + "");
const split = dataProvider.split("?");
let endpoint = split.shift();
const searchParams = new URLSearchParams(split.join("?"));
const filterData: Record = this.filterPublisher?.get();
const filteredFieldsArray = this.filteredFields.split(" ");
for (const f in filterData) {
if (
(this.filteredFields && filteredFieldsArray.includes(f)) ||
filterData[f] == null ||
filterData[f] == ""
)
continue;
searchParams.set(f, filterData[f]);
}
if (!this.searchHash) this.searchHash = searchParams.toString();
endpoint = endpoint + "?" + searchParams.toString();
dataProvider = dataProvider + "_item_from_queue_" + this.instanceId;
this.listDataProviders.push(dataProvider);
const newProps: QueueProps = [
...this.props,
{
id: searchParams.toString() + "/" + this.props.length,
dataProvider: dataProvider,
endPoint: endpoint,
offset: offset,
limit: this.limit,
},
];
newProps.resultCount = this.resultCount;
newProps.lastFetchedData = e?.detail.fetchedData || {};
this.props = newProps;
this.lastRequestTime = new Date().getTime();
}
storedScrollY = 0;
storedScrollX = 0;
render() {
this.storeScrollPosition();
if (!Array.isArray(this.props)) {
return nothing;
}
let lazyload = !this.noLazyload;
if (this.props.length == 1) {
lazyload = false;
}
this.style.display = "block";
return html`
${map(this.props, (item, index) => {
const templates =
index == 0
? this.templates
: this.templates?.filter(
(elt) => elt.getAttribute("data-value") != "no-item"
);
return html`
`;
})}
`;
}
}