//@ts-check
import Queue from "../../../utils/abstract-data-types/queue/queue";
import RandomNumberCharGenUtils from "../../../utils/random-number-generator/random_number_char_generator";
import ListReverser from "../../../utils/lists/list_reverser";
import DataManager from "../../data_manager";
import ListDataPaginator from "../../paginators/list_data_paginator";
import StandardViewManager from "../standard/standard_view_manager";
/**
* @template {any} M
* @template {ArrayOnlyNestedKeys<M>} G_S
*
* @extends {StandardViewManager<M, G_S>}
*/
class ListViewManager extends StandardViewManager{
/**
* @param {DataManagerInstance<M>} dataManager
* @param {import("ListViewManager").ListDataManagerViewManagerConstructorOptions<M, G_S>} options
*/
constructor(dataManager, options){
super(dataManager, options);
//VERY IMPORTANT setting. Will allow spawned models to be removed with the right mutation
this.rootViewOptions = { ...options.viewOptions, reinflateRootViewOnNewModel: true };
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['existingModelsList']}
*/
this.existingModelsList = null;
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['filteredInternalModelsList']}
*/
this.filteredInternalModelsList = [];
/**
* @type {ListDataPaginatorInstance<M, G_S>}
*/
this.listDataPaginator = null;
/**
* @type {Element}
*/
this.overrideObserverViewPort = null;
//for telling about server-side pagination status
this.existingModelsOptions = {
continuePaginationForExisting: options.continuePaginationForExisting
};
//Set up pagination
if(options.pagination?.enabled){
this.setUpPaginator(options.pagination);
}
}
/**
* Allows self to be child. Else, throws error
*/
validateSelfAsChild(){
return true;
}
/**
* @this ListViewManagerInstance<M, G_S>
* Call your sets AFTER needed main view is bound
*
* Get invokation from onViewReady calls for lifecycle manager to do these automatically,
* including running server side
*
* if we have data, and no server side to process, then spawn directly (autopopulate)
*/
autoPopulateViewsFromExistingOrServerSide(){
if(this.dataManager.hasData()){
//DO NOT REVERSE THE DATA
//FOR LISTS, JUST REVERSE VIEW LISTS IF STACK, SO WELL REPRESENTED WITH DATA.
//BUT NEVER REVERSE DATA. Must be congruent with model
/**
* @type {string | string[]}
*/
let modelId_s = null;
//if child, build for child
if(this.isChildInfo.isChild){
//The data we have to work with, given the parent's modelID
modelId_s = this.childOptions.parentModelId;
const data = this.reduceHookDataToScope(this.dataManager.getModel(modelId_s), "MODEL_ROOT", this.scope, null);
this.existingModelsList = data;
} else {
//Its a parent. Source everything in scope
//Model id will also be an array
if(this.scope === DataManager._MODEL_ROOT_SCOPE){
this.existingModelsList = [];
modelId_s = [];
for(let i = 0; i < this.dataManager.dataLength(); i++){
//No need to reduce. Data scope of MODEL_ROOT same as view manager. So direct push
this.existingModelsList.push(this.dataManager.getModelInIndex(i));
modelId_s.push(this.dataManager.getModelId(i));
}
} else {
//By default, will work with the first model in index for that scope.
//Yes. Cause its parent, and no information about changing index
//This is for a weird scenario. Warn
console.warn(`LIST VIEW MANAGER ID: ${this.id}. List view manager is not a child and scope is not MODEL_ROOT. Taking relevant scope in first model (index 0)`);
console.error("In autoPopulateViewsFromExisting, after serverSide trigger");
modelId_s = this.dataManager.getModelId(0);
const data = this.reduceHookDataToScope(this.dataManager.getModelInIndex(0), "MODEL_ROOT", this.scope, null);
this.existingModelsList = data;
}
}
//Now, autopopulate views, if not already server-side generated by calling onCommit
if(Array.isArray(this.existingModelsList)){
//direct call commit if child. Else, just leave as is. Parent already invoked child. Just getting data
if(!this.isChildInfo.isChild){
//using this.processServerSide flag to automatically populate if not server side but rather an init direct
this.onCommit("create", this.existingModelsList, null, null, modelId_s, this.scope, this.scope, () => {}, { isServerSideCreate: this.processServerSide, paginationInfo: { [this.scope]: { stopPagination: this.existingModelsOptions.continuePaginationForExisting?.(this.existingModelsList, modelId_s) } } });
}
} else {
console.error(`Cannot autopopulate views. Your scope ${this.scope} doesn't refer to an array`);
}
}
}
/**
* Hooks any server-side views, if server side
* @returns {ServerSideAttachedViewInfo[]}
*/
hookServerSideViews(){
if(this.processServerSide){
//Child is not automatically triggered
//So, trigger first, as this trigger was from onCommit, invoke children, in parent
if(this.isChildInfo.isChild){
this.autoPopulateViewsFromExistingOrServerSide();
}
//Just fail if we don't have a valid existingModelsList
if(!this.existingModelsList?.length){
throw new Error("Failed to attach server side views to view manager. No models loaded to list");
}
//Get the views
let serverSideViews = this.getParentRootViewNode().querySelectorAll(`.${this.rootViewOptions.componentViewClass}`);
if(this.baseViewAppendOrder === "stack"){
serverSideViews = ListReverser.reverseList(serverSideViews);
}
if(this.existingModelsList.length < serverSideViews.length){
throw new Error(`Can't attach server side views for list manager with id ${this.id}. Models and views length not equal. Have you sourced them over network? Init calls are delayed for this. \n\nModel: ${this.existingModelsList.length} :: ${serverSideViews.length}`);
}
if(this.existingModelsList.length > serverSideViews.length){
//throw an error?
console.warn(`Attaching server side views for list manager with id ${this.id}. However, note that internal models list length exceeds server side views length: ${serverSideViews.length}`);
}
/**
* @type {AttachServerSideViewInfo[]}
*/
this.finalAttachServerSideInfo = [];
if(this.isChildInfo.isChild){
//add to attachedModels
serverSideViews.forEach((view, index) => {
const mappedId = this.spawnAttachedModels(this.childOptions.parentModelId, view);
//put in info
this.finalAttachServerSideInfo.push({
mappedDataId: mappedId,
viewNode: view,
modelId: this.childOptions.parentModelId
});
//Fire hooks
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
//calling this cause that's where you attach listeners and etc
//indexing with index cause it is array (that's the base type of a list view manager's scope)
rootViewDataHooks.root.builder.onViewAttach(this.childOptions.parentModelId, this.dataManager.getScopedModel(this.scope, mappedId, this.childOptions.parentModelId, false)[index], view, mappedId, { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
});
} else {
if(this.scope === DataManager._MODEL_ROOT_SCOPE){
//Add to attached models
serverSideViews.forEach((view, index) => {
const modelID = this.dataManager.getModelId(index);
const mappedId = this.spawnAttachedModels(modelID, view);
//put in info
this.finalAttachServerSideInfo.push({
mappedDataId: mappedId,
viewNode: view,
modelId: modelID
});
//Fire hooks
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
rootViewDataHooks.root.builder.onViewAttach(modelID, this.dataManager.getModel(modelID), view, mappedId, { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
});
} else {
//By default, will work with the first model in index for that scope.
//Yes. Cause its parent, and no information about changing index
//This is for a weird scenario. Warn
console.warn(`LIST VIEW MANAGER ID: ${this.id}. List view manager is not a child and scope is not MODEL_ROOT. Taking relevant scope in first model (index 0)`);
//Add to attached models
serverSideViews.forEach((view, index) => {
const modelID = this.dataManager.getModelId(0);
const mappedId = this.spawnAttachedModels(modelID, view);
//put in info
this.finalAttachServerSideInfo.push({
mappedDataId: mappedId,
viewNode: view,
modelId: modelID
});
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
rootViewDataHooks.root.builder.onViewAttach(modelID, this.dataManager.getScopedModel(this.scope, mappedId, modelID, false)[index], view, mappedId, { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
});
}
}
return this.finalAttachServerSideInfo;
} else {
throw new Error("Called to process server side while flag is false");
}
}
/**
* @type {StandardViewManagerInstance<M, G_S>['getParentRootViewNode']}
*/
getParentRootViewNode(){
!this.parentRootViewNode ? this.parentRootViewNode = !this.isChildInfo.isChild ? document.getElementById(this.rootViewOptions.parentNodeID) : this.childOptions.parentRootNode.querySelector(`.${this.rootViewOptions.parentNodeClass_AsChild}`) : null;
if(!this.parentRootViewNode){
if(!this.isChildInfo.isChild){
console.error("DATA MANAGER VIEW MANAGER: Attempted to run root and view node build but parent with id " + this.rootViewOptions.parentNodeID + " has not been manually attached to the view. Ensure it already exists in markup");
} else {
console.error("DATA MANAGER VIEW MANAGER: Attempted to run root and view node build but node in parent view manager has not been manually attached to the view. Ensure it already exists in markup.\nClass selector: " + this.rootViewOptions.parentNodeClass_AsChild);
}
return null;
}
return this.parentRootViewNode;
}
/**
* TODO: Spawn children only if we have a view node (so not a temp)
* For temp, spawn on updateAttachedModels
* @type {StandardViewManager<M, G_S>['spawnAttachedModels']}
*/
spawnAttachedModels(modelId, attachedViewNode){
//runs the same even for fake spawn. Just updates
const getMappedId = () => {
return `${this.id}#${RandomNumberCharGenUtils.generateRandomNumChar(6)}`;
}
const mappedId = getMappedId();
//item position is okay analogous to length of attached models length, since managing an array
//but when getting orderedArrayIndices, parent joins to this. Works well
this.attachedModels.enqueue({
modelId: modelId,
itemPosition: this.attachedModels.length,
mappedDataId: mappedId,
attachedViewNode: attachedViewNode,
});
//Set the child and watchers. Outside so we just get the okay ordered indices
this.attachedModels.peek().orderedChildViewManagers = this.inflateOrderedChildViewManagers({ parentDataIndex: this.attachedModels.length, parentModelId: modelId, parentRootNode: attachedViewNode, parentOrderedArrayIndices: this.getOrderedArrayIndices(this.scope, mappedId) }), //No enqueue. Working now after commit
this.attachedModels.peek().watcherChildViewManagers = this.getWatcherChildViewManagers({ parentRootNode: this.getParentRootViewNode() })
return mappedId;
}
/**
*
* So, have two versions. CORRECT WAY AS PER ALGO is to return a non-empty queue
*
* Scope used to determine if ordered array indices valid (only if scope not model root)
* @type {StandardViewManagerInstance<M, G_S>['getOrderedArrayIndices']}
*/
getOrderedArrayIndices(scope, mappedDataId){
/**
* @type {ViewManagerOrderedArrayIndices}
*/
const orderedArrayIndices = new Queue();
//if scope is model root, just return an empty queue. modelId is the target lol
if(this.scope === DataManager._MODEL_ROOT_SCOPE){
return orderedArrayIndices;
}
//if child, add from parents
this.childOptions?.parentOrderedArrayIndices ? orderedArrayIndices.join(this.childOptions?.parentOrderedArrayIndices.copy()) : null;
//Now join with result from self or children
orderedArrayIndices.join(super.getOrderedArrayIndices(scope, mappedDataId));
return orderedArrayIndices;
}
/**
* @type {StandardViewManagerInstance<M, G_S>['onCommit']}
*/
onCommit(mutation, newData, oldData, mappedDataId, modelId, APIScope, originalScope, completeCb, extras){
//bounce completely if requested scope is not same, child, or parent of this
if(this.canRunHooks(APIScope)){
if(this.canRunHooks(APIScope, true)){
//create clones
newData = structuredClone(newData);
oldData = structuredClone(oldData);
const origModelId_s = modelId;
modelId = structuredClone(modelId);
//Dealing with edge loadNew cases where response 200 OK but no data
if(modelId){
const newViewManagerScopedData = this.reduceHookDataToScope(newData, APIScope, this.scope, mappedDataId);
const oldViewManagerScopedData = this.reduceHookDataToScope(oldData, APIScope, this.scope, mappedDataId);
/**
* @param {boolean} modelIdFixed
* @param {genericFunction} completeCB
* @param {boolean} [doneFirstPass]
*/
//Create clones for our recursive actions (since array)
const recursiveOrigNewData = structuredClone(newData);
const recursiveOrigOldData = structuredClone(oldData);
//Usually array if working in same scope, or MODEL_ROOT with mutiple models
//else, can have situation where new data is object, but scoped data (to this scope) is the array
//That's why the scopedData array functions are valid and untouched
const origDataIsArray = Array.isArray(recursiveOrigNewData);
//get the attached server side views
/**
* @type {ServerSideAttachedViewInfo[]}
*/
let serverSideAttachedViewInfo = [];
if(extras?.isServerSideCreate){
serverSideAttachedViewInfo = this.hookServerSideViews();
}
const recursiveCallCommits = (modelIdFixed, completeCB, doneFirstPass) => {
if((Array.isArray(modelId) && modelId.length) || (modelIdFixed && newViewManagerScopedData.length)){
const finalModelID = modelIdFixed ? modelId : modelId.at(0);
this.runSingleCommit(mutation, newViewManagerScopedData.at(0), oldViewManagerScopedData?.at(0), mappedDataId, finalModelID, APIScope, originalScope, () => {
newViewManagerScopedData.splice(0, 1);
oldViewManagerScopedData?.splice(0, 1);
modelIdFixed ? null : modelId.splice(0, 1);
if(origDataIsArray){
//reduce the cloned new and old data
recursiveOrigNewData.splice(0, 1);
recursiveOrigOldData?.splice(0, 1);
}
if(serverSideAttachedViewInfo.length){
//reduce the serverSideAttachedViewInfo
serverSideAttachedViewInfo.splice(0, 1);
}
recursiveCallCommits(modelIdFixed, completeCB, true);
//extras can be undefined cause of non "new" mutations
}, { ...extras, origOldData_At: origDataIsArray ? recursiveOrigOldData?.at(0) : recursiveOrigOldData, origNewData_At: origDataIsArray ? recursiveOrigNewData.at(0) : recursiveOrigNewData, tempMappedDataIdInfo: { ...extras?.tempMappedDataIdInfo, tempMappedDataId: doneFirstPass ? null : extras?.tempMappedDataIdInfo?.tempMappedDataId }, serverSideAttachedInfo: serverSideAttachedViewInfo.at(0) });
} else {
//set up paginator here?
//if not delete or delete_FLushAll. Those self-manage
if(mutation === "loadNew" || mutation === "uploadNew" || mutation === "create"){
let stopPagination = false;
if(extras?.isServerSideCreate && this.isChildInfo.isChild){
//child never fires this directly before onCommit because a child's mutation is fired by parent, but a parent fires its own when spawning from existing
stopPagination = this.existingModelsOptions.continuePaginationForExisting?.(this.existingModelsList, origModelId_s);
} else {
stopPagination = extras?.paginationInfo ? extras.paginationInfo[this.scope]?.stopPagination : undefined
}
this.setUpPaginationIntersector(mutation, modelId, stopPagination);
}
completeCB();
}
}
//Call the prePostRootAttachHooks
if(mutation === "uploadNew" || mutation === "loadNew" || mutation === "create"){
if(mappedDataId){
console.error(`Received a mapped data ID for view manager ${this.id} and scope ${this.scope} | APIScope: ${this.scope} for a "new" mutation ${mutation}`);
console.warn(`Will safely assume this specific view manager's implementation has no child. So, can run mutation here and nullify provided mapped data id`);
mappedDataId = null;
}
//Called b4 views are spawned. Good call
if(!extras?.isServerSideCreate){
//Call for root view hooks
if(this.rootViewDataHooks && (this.dataComparator(newData, oldData, APIScope, mappedDataId).base() || mutation === "create")){
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
//Passes the parent root node, not viewRoot
rootViewDataHooks.root.prePostRootAttachHooks.onCommit(modelId, mutation, newViewManagerScopedData, oldViewManagerScopedData, this.getParentRootViewNode(), { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
}
}
}
if(Array.isArray(modelId)){
//Allows for say bulk delete try, and API can failsafe the transaction i.e delete valid ones and skip non-valid ones
//And you can provide that here. just match newData length to model id length of deleted
//Counter-intuitive, but makes a lot of sense to work like this, functionally
if((mutation === "loadNew" || mutation === "delete") && newViewManagerScopedData?.length !== modelId.length){
console.error("Length of new data and new model ids should match if mutation is loadNew or delete " + newViewManagerScopedData?.length + " :: " + modelId.length);
completeCb();
return;
}
recursiveCallCommits(false, () => {
completeCb();
});
} else {
//Change happened to one model id.
//Depending on request scope, Data an array or not (selfType) - YES. So valid, but deeper scope (inside array)
//All from server-side are array though, cause we retrieve whole list (to scope) - figure out commit
//delete mutation (for bulk) should NEVER get here, because it also provides an array of model id
if(Array.isArray(newViewManagerScopedData)){
recursiveCallCommits(true, () => {
if((mutation === "delete" || mutation === "delete_FlushAll") && (originalScope === DataManager._MODEL_ROOT_SCOPE || originalScope === this.scope || APIScope === this.scope)){
// this.setUpPaginationIntersector(mutation);
// completeCb();
} else {
// runChildrenViewManagerCommits();
}
//just complete
completeCb();
});
} else {
//Will NEVER be the case for server side attached views
//Always return an array as data
this.runSingleCommit(mutation, newViewManagerScopedData, oldViewManagerScopedData, mappedDataId, modelId, APIScope, originalScope, () => {
//doing this check first because for delete, we run children first - also, such mutations only pass null, so expect this
if((mutation === "delete" || mutation === "delete_FlushAll") && (originalScope === DataManager._MODEL_ROOT_SCOPE || originalScope === this.scope || APIScope === this.scope)){
// this.setUpPaginationIntersector(mutation);
// completeCb();
} else {
// runChildrenViewManagerCommits();
}
//just complete
completeCb();
}, { ...extras, origOldData_At: oldData, origNewData_At: newData });
}
}
} else {
if(this.rootViewDataHooks){
console.warn("Committed with no modelID. This should only be valid for a loadNew mutation where the API returns a 200 OK response but no data...EEEH NOPE");
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
//Passes the parent root node, not viewRoot
rootViewDataHooks.root.prePostRootAttachHooks.onCommit(null, mutation, null, null, this.getParentRootViewNode(), { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
}
completeCb();
}
} else {
//transfer onCancel logic (take arrays and recursive call) then short test this scope thing for now
//then get back to the main job. We need to be up tomorrow morning
if(!mappedDataId){
/**
* Read more in onMutate, same place
*/
throw new Error(`Data operation is of a specific scope, but no mapped data id provided.\nScope: ${this.scope} | APIScope: ${APIScope}`);
}
this.invokeChildViewManagers(mappedDataId, (viewManager, cb) => {
viewManager.onCommit(mutation, newData, oldData, mappedDataId, modelId, APIScope, originalScope, () => {
cb();
}, { ...extras, tempMappedDataIdInfo: extras?.tempMappedDataIdInfo?.childrenInfo.find((info) => info.viewManagerId === viewManager.id) });
}, () => {
completeCb();
});
}
} else {
completeCb();
}
}
/**
* @template {keyof DataManagerInstance<M>['masterWorkingModel']['scopedOptions']['apis']} FinalScope
* @param {DataManagerMutations} mutation
* @param {Partial<ValueTypeOfArrayOnly<M, G_S>>} newData
* @param {Partial<ValueTypeOfArrayOnly<M, G_S>>} oldData
* @param {string} mappedDataId
* @param {string} modelId
* @param {FinalScope} APIScope
* @param {NestedChildKeyOf<M, FinalScope>} originalScope
* @param {genericFunction} completeCb
* @param {ViewManagerMutationCBExtras<M, FinalScope> & { origOldData_At: DataManagerPermittedBulkType<M, FinalScope>, origNewData_At: DataManagerPermittedBulkType<M, FinalScope>, serverSideAttachedInfo: ServerSideAttachedViewInfo }} extras
*/
runSingleCommit(mutation, newData, oldData, mappedDataId, modelId, APIScope, originalScope, completeCb, extras){
if((mutation === "delete" || mutation === "delete_FlushAll") && (originalScope === DataManager._MODEL_ROOT_SCOPE || originalScope === this.scope || APIScope === this.scope)){
//tell the children first
this.invokeChildViewManagers(mappedDataId,(viewManager, cb) => {
viewManager.onCommit(mutation, extras.origNewData_At, extras.origOldData_At, mappedDataId, modelId, APIScope, originalScope, () => {
cb();
}, extras);
}, () => {
//Now remove self
//If the APIScope is model root or matches this scope data is being deleted. View no longer relevant
//Else, normal mutation flow followed
//If the APIScope is model root or matches this scope data is being deleted. View no longer relevant
//Else, normal mutation flow followed
if(APIScope === this.scope || this.scope.toString().startsWith(APIScope)){
this.detachViewNodeFromRootParent(mutation, modelId, newData, mappedDataId);
completeCb();
}
});
} else {
/**
* @type {import("StandardViewManager").NewViewInfo}
*/
let newViewInfo = null;
if(mutation === "uploadNew" || mutation === "loadNew" || mutation === "create"){
if(mappedDataId){
//add for create in commit
throw new Error("Cannot have a mappedDataId for loadNew, uploadNew, or create in onCommit. No view bound to operation yet as model or scoped model being created");
}
if(mutation === "create"){
if(!extras.isServerSideCreate){
//Spawn new root or view node
newViewInfo = this.runRootAndViewNodeBuild(mutation, modelId, newData);
} else {
//get from spawned in serverSideCreate
newViewInfo = extras.serverSideAttachedInfo;
}
} else {
newViewInfo = this.runRootAndViewNodeBuild(mutation, modelId, newData, extras.tempMappedDataIdInfo?.tempMappedDataId);
}
mappedDataId = newViewInfo.mappedDataId;
}
//No need for else because of loadNew, create, or uploadNew, will build root so view components need to be updated
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['componentViewDataHooks']}
*/
//@ts-expect-error
const componentViewDataHooks = this.componentViewDataHooks;
//Pass this scope to match req scope. Ensure data also is original
this.invokeRootComponentHooks(mutation, extras.origNewData_At, extras.origOldData_At, mappedDataId, modelId, APIScope, (targetScope, newScopedData) => {
componentViewDataHooks[targetScope].hooks.onCommit?.(modelId, mutation, newData, oldData, newScopedData, this.getViewNodeForMappedDataId(mappedDataId), mappedDataId, { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
}, () => {
//children invoked AFTER
this.invokeChildViewManagers(mappedDataId, (viewManager, cb) => {
viewManager.onCommit(mutation, extras.origNewData_At, extras.origOldData_At, mappedDataId, modelId, APIScope, originalScope, () => {
cb();
}, { ...extras, tempMappedDataIdInfo: extras?.tempMappedDataIdInfo?.childrenInfo.find((info) => info.viewManagerId === viewManager.id) });
}, () => {
completeCb();
});
});
}
}
//Method to init intersection observer for pagination based on mutation....
//for delete, have to update on each delete
//For recycle view, do update for each completed spawn of views, which can happen without loadNew
//mutation, but a scroll action as well
//USE SCROLL HEIGHT btw to determine maximum size of view and have a scroller size determined by length,
//and travel equalling scroll by same and data length
//FOR RECYCLE, THIS TRIGGERS ARE DEPENDENT if the viewNode is at pos close to model length
//Don't just fire hapharzadly - override these for recycle to control logic
/**
*
* @param {DataManagerMutations} mutation
* @param {string} modelId
* @param {boolean} stopPagination
*/
setUpPaginationIntersector(mutation, modelId, stopPagination){
if(mutation === "uploadNew" || mutation === "loadNew" || mutation === "create"){
this.listDataPaginator?.setSoleModelId(modelId);
if(stopPagination !== undefined){
this.listDataPaginator?.setPaginationComplete(stopPagination);
}
this.listDataPaginator?.setUpPaginationIntersector();
}
}
/**
* @param {Element} deletedNode
*/
updatePaginationIntersectorOnDelete(deletedNode){
this.listDataPaginator?.updatePaginationIntersectorOnDelete(deletedNode);
}
/**
* @override
* @type {StandardViewManager<M, G_S>['runRootAndViewNodeBuild']}
* @param {DataManagerMutations} mutation
* @param {Partial<ValueTypeOfArrayOnly<M, G_S>>} data
* @param {string} modelId
* @returns {import("StandardViewManager").NewViewInfo}
*/
//@ts-expect-error
runRootAndViewNodeBuild(mutation, modelId, data, overrideSpawnedMappedDataId){
if(this.getParentRootViewNode() && this.rootViewDataHooks && this.rootViewOptions && data){ //Must have data to run
/**
* @type {Element}
*/
let viewNode = null;
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['rootViewDataHooks']}
*/
//@ts-expect-error
const rootViewDataHooks = this.rootViewDataHooks;
//Attach new child
//Defaults are, list for baseViewAppendOrder and bottom for create new
const template = rootViewDataHooks.root.builder.inflateRoot(data);
const defaultAttachment = this.baseViewAppendOrder === "stack" ? "afterbegin" : "beforeend";
this.parentRootViewNode.insertAdjacentHTML(mutation === "create" ? this.baseViewCreateNewPos === "top" ? "afterbegin" : "beforeend" : defaultAttachment, template.inflatedView);
//Get the view node and update
const allChildren = this.parentRootViewNode.getElementsByClassName(this.rootViewOptions.componentViewClass);
let newNodePos = null;
if(mutation === "create"){
newNodePos = this.baseViewCreateNewPos === "top" ? 0 : allChildren.length - 1;
} else {
newNodePos = this.baseViewAppendOrder === "stack" ? 0 : allChildren.length - 1;
}
viewNode = allChildren[newNodePos];
//Update attached models list - the main list we look at and reference to match data
const mappedDataId = overrideSpawnedMappedDataId ? this.updateAttachedModel(overrideSpawnedMappedDataId, viewNode, modelId) : this.spawnAttachedModels(modelId, viewNode);
rootViewDataHooks.root.builder.onViewAttach(modelId, data, viewNode, mappedDataId, { viewManagerRef: this, parentMappedDataId: this.childOptions.parentMappedDataId });
return { viewNode: viewNode, mappedDataId: mappedDataId };
} else {
if(!this.rootViewDataHooks){
console.warn("DATA MANAGER VIEW MANAGER: Attempted to run initial root build but no root view hooks provided");
}
if(!this.rootViewOptions){
console.warn("DATA MANAGER VIEW MANAGER: Attempted to run initial root build but no root view options provided");
}
}
}
/**
* The implementation below is for standard only
* @type {StandardViewManagerInstance<M, G_S>['detachViewNodeFromRootParent']}
*/
detachViewNodeFromRootParent(mutation, modelId, data, mappedDataId){
if(mutation === "delete_FlushAll"){
//Hard clear
this.getParentRootViewNode().innerHTML = "";
//Doing this to trigger a whole unobserve. Will not find because all views gone, so clear references
this.updatePaginationIntersectorOnDelete(this.attachedModels.peek()?.attachedViewNode);
this.attachedModels.clear();
} else {
if(!mappedDataId){
mappedDataId = this.getMappedDataIdThatMatchesProperties(data);
}
const targetViewNode = this.getViewNodeForMappedDataId(mappedDataId);
if(targetViewNode){
//Invoke the correct hook
if(this.rootViewDataHooks){
this.rootViewDataHooks.root.builder.onViewDetach(modelId, data, targetViewNode, mappedDataId, () => {
//detach current child
this.getParentRootViewNode().removeChild(targetViewNode);
});
//clear attached models
this.attachedModels.sortDelete(this.attachedModels.find((model) => model.mappedDataId === mappedDataId));
}
this.updatePaginationIntersectorOnDelete(targetViewNode);
} else {
console.error("Cannot detach view node. Not found for mappedDataId " + mappedDataId);
}
}
}
/**
* @type {import("ListViewManager").ListViewManagerInstance<M, G_S>['registerViewDataHooks']}
*/
//@ts-expect-error
registerViewDataHooks(rootHooks, componentHooks){
//@ts-expect-error
return super.registerViewDataHooks(rootHooks, componentHooks);
}
/**
* @type {ListViewManagerInstance<M, G_S>['getViewNodeAtDataPos']}
*/
getViewNodeAtDataPos(pos){
if(this.attachedModels){
const targetModel = this.attachedModels.find((model) => model.itemPosition === pos);
if(targetModel){
return targetModel.attachedViewNode
}
}
return null;
}
/**
* @type {ListViewManagerInstance<M, G_S>['dataLength']}
*/
dataLength(){
return this.existingModelsList?.length;
}
/**
* @type {ListViewManagerInstance<M, G_S>['setUpPaginator']}
*/
setUpPaginator(options){
if(this.listDataPaginator){
return false;
} else {
//Inflate
/**
* @type {ListDataPaginatorInstance<M, G_S>}
*/
this.listDataPaginator = new ListDataPaginator({
...options,
viewOptions: this.rootViewOptions,
dataManagerInstance: this.dataManager,
listViewManagerInstance: this,
scope: this.scope,
soleModelId: this.isChildInfo.isChild ? this.childOptions.parentModelId : this.scope === DataManager._MODEL_ROOT_SCOPE ? DataManager._MODEL_ROOT_SCOPE : null
});
this.overrideObserverViewPort = options.observerViewPort;
return true;
}
}
/**
* @type {ListViewManagerInstance<M, G_S>['getListDataPaginator']}
*/
getListDataPaginator(){
return this.listDataPaginator;
}
/**
* @type {ListViewManagerInstance<M, G_S>['getIntersectionObserverViewPort']}
*/
getIntersectionObserverViewPort(){
return this.overrideObserverViewPort || this.getParentRootViewNode();
}
}
if(false){
/**
* @type {import("ListViewManager").ListViewManagerInstance<*, *>}
*/
const check = new ListViewManager();
}
export default ListViewManager;