"use strict";
class ListUtil {
static _pGetSublistEntities_getCount ({ser}) { return isNaN(ser.c) ? 1 : Number(ser.c); }
static async pGetSublistEntities_fromList ({exportedSublist, dataList, page}) {
if (!exportedSublist?.items) return [];
page = page || UrlUtil.getCurrentPage();
return (await exportedSublist
.items
.pSerialAwaitMap(async ser => {
const listItem = Hist.getActiveListItem(ser.h);
if (listItem == null) return null;
const entity = await Renderer.hover.pApplyCustomHashId(
page,
// Pull from the list page, as there may be list-page-specific temp data
dataList[listItem.ix],
// Support lowercase prop from URL
ser.customHashId || ser.customhashid,
);
return {
count: this._pGetSublistEntities_getCount({ser}),
entity,
ser,
};
}))
.filter(Boolean);
}
static async pGetSublistEntities_fromHover ({exportedSublist, page}) {
if (!exportedSublist?.items) return [];
page = page || UrlUtil.getCurrentPage();
return (await exportedSublist
.items
.pSerialAwaitMap(async ser => {
let entity = await DataLoader.pCacheAndGetHash(page, ser.h);
if (!entity) return null;
entity = await Renderer.hover.pApplyCustomHashId(
page,
entity,
// Support lowercase prop from URL
ser.customHashId || ser.customhashid,
);
if (!entity) return null;
return {
count: this._pGetSublistEntities_getCount({ser}),
entity,
};
}))
.filter(Boolean);
}
static getWithoutManagerState (saveEntity) {
return this._getWithoutManagerState({saveEntity, prefix: "manager_"});
}
static getWithoutManagerClientState (saveEntity) {
return this._getWithoutManagerState({saveEntity, prefix: "managerClient_"});
}
static _getWithoutManagerState ({saveEntity, prefix}) {
if (!saveEntity) return saveEntity;
const cpy = MiscUtil.copyFast(saveEntity);
Object.keys(cpy)
.filter(k => k.startsWith(prefix))
.forEach(k => delete cpy[k]);
return cpy;
}
static getDownloadFiletype ({page}) {
page = page || UrlUtil.getCurrentPage();
return `${page.replace(".html", "")}-sublist`;
}
static getDownloadName ({page, save}) {
return `${this.getDownloadFiletype({page})}${save.entity.name ? `-${save.entity.name}` : ""}`;
}
static getDownloadFiletypeSaves ({page}) {
page = page || UrlUtil.getCurrentPage();
return `${page.replace(".html", "")}-sublist-saves`;
}
static getDownloadNameSaves ({page}) {
return this.getDownloadFiletypeSaves({page});
}
}
class ListUtilEntity {
static _getString_action_currentPinned_name ({page}) { return `From Current ${UrlUtil.pageToDisplayPage(page)} Pinned List`; }
static _getString_action_savedPinned_name ({page}) { return `From Saved ${UrlUtil.pageToDisplayPage(page)} Pinned List`; }
static _getString_action_file_name ({page}) { return `From ${UrlUtil.pageToDisplayPage(page)} Pinned List File`; }
static _getString_action_currentPinned_msg_noSaved ({page}) { return `No saved list! Please first go to the ${UrlUtil.pageToDisplayPage(page)} page and create one.`; }
static _getString_action_savedPinned_msg_noSaved ({page}) { return `No saved lists were found! Go to the ${UrlUtil.pageToDisplayPage(page)} page and create some first.`; }
static async _pGetLoadableSublist_getAdditionalState ({exportedSublist}) { return {}; }
static async pGetLoadableSublist ({exportedSublist, page}) {
if (exportedSublist == null) return null;
const entityInfos = await ListUtil.pGetSublistEntities_fromHover({exportedSublist, page});
const additionalState = await this._pGetLoadableSublist_getAdditionalState({exportedSublist});
return {
entityInfos,
...additionalState,
};
}
static async _pHandleExportedSublist_pMutAdditionalState ({exportedSublist}) { /* Implement as required */ }
static async _pHandleExportedSublist (
{
pFnOnSelect,
page,
evt,
exportedSublist,
isReferencable,
...others
},
) {
if (exportedSublist == null) return;
const loadableSublist = await this.pGetLoadableSublist({exportedSublist, page});
await pFnOnSelect({
isShiftKey: evt?.shiftKey,
...others,
isReferencable,
exportedSublist,
...loadableSublist,
});
}
static _getFileTypes ({page}) {
return [ListUtil.getDownloadFiletype({page})];
}
static async _pHandleClick_loadSublist_currentPinned (
{
pFnOnSelect,
page,
evt,
...others
},
) {
const sublistPersistor = new SublistPersistor({page});
const exportedSublist = await sublistPersistor.pGetStateFromStorage();
if (!exportedSublist) {
return JqueryUtil.doToast({
content: this._getString_action_currentPinned_msg_noSaved({page}),
type: "warning",
});
}
await this._pHandleExportedSublist({pFnOnSelect, page, exportedSublist, evt, ...others});
}
static async _pHandleClick_loadSublist_savedPinned (
{
pFnOnSelect,
optsSaveManager,
page,
evt,
...others
},
) {
const saveManager = new SaveManager({
isReadOnlyUi: true,
page,
...optsSaveManager,
});
await saveManager.pMutStateFromStorage();
if (!(await saveManager.pHasSaves())) {
return JqueryUtil.doToast({
type: "warning",
content: this._getString_action_savedPinned_msg_noSaved({page}),
});
}
const exportedSublist = await saveManager.pDoLoad({isIncludeManagerClientState: true});
if (!exportedSublist) return;
await this._pHandleExportedSublist({pFnOnSelect, page, exportedSublist, evt, ...others});
}
static async _pHandleClick_loadSublist_file (
{
pFnOnSelect,
page,
evt,
...others
},
) {
const {jsons, errors} = await InputUiUtil.pGetUserUploadJson({
expectedFileTypes: this._getFileTypes({page}),
});
DataUtil.doHandleFileLoadErrorsGeneric(errors);
if (!jsons?.length) return;
const json = jsons[0];
await this._pHandleExportedSublist({pFnOnSelect, page, exportedSublist: json, evt, ...others});
}
static getContextOptionsLoadSublist (
{
pFnOnSelect,
optsSaveManager,
optsFromCurrent,
optsFromSaved,
optsFromFile,
page,
},
) {
if (!page) throw new Error(`Missing required "page" arg!`);
return [
new ContextUtil.Action(
this._getString_action_currentPinned_name({page}),
evt => this._pHandleClick_loadSublist_currentPinned({pFnOnSelect, page, evt}),
{
...optsFromCurrent || {},
},
),
new ContextUtil.Action(
this._getString_action_savedPinned_name({page}),
evt => this._pHandleClick_loadSublist_savedPinned({pFnOnSelect, optsSaveManager, page, evt}),
{
...optsFromSaved || {},
},
),
new ContextUtil.Action(
this._getString_action_file_name({page}),
evt => this._pHandleClick_loadSublist_file({pFnOnSelect, page, evt}),
{
...optsFromFile || {},
},
),
];
}
static async pDoUserInputLoadSublist (
{
pFnOnSelect,
optsSaveManager,
page,
optsFromCurrent,
optsFromSaved,
optsFromFile,
altGenerators,
},
) {
const values = [
optsFromCurrent?.renamer ? optsFromCurrent.renamer(this._getString_action_currentPinned_name({page})) : this._getString_action_currentPinned_name({page}),
optsFromSaved?.renamer ? optsFromSaved.renamer(this._getString_action_savedPinned_name({page})) : this._getString_action_savedPinned_name({page}),
optsFromFile?.renamer ? optsFromFile.renamer(this._getString_action_file_name({page})) : this._getString_action_file_name({page}),
];
const ixdPFnConfirms = [
optsFromCurrent?.pFnConfirm,
optsFromSaved?.pFnConfirm,
optsFromFile?.pFnConfirm,
];
const ixdOtherOpts = [...new Array(3)].map(() => {});
if (altGenerators?.length) {
altGenerators.forEach(({fromCurrent, fromSaved, fromFile}) => {
const modes = [fromCurrent, fromSaved, fromFile];
modes.forEach(mode => {
ixdPFnConfirms.push(mode?.pFnConfirm);
ixdOtherOpts.push(mode?.otherOpts || {});
});
values.push(fromCurrent.renamer(this._getString_action_currentPinned_name({page})));
values.push(fromSaved.renamer(this._getString_action_savedPinned_name({page})));
values.push(fromFile.renamer(this._getString_action_file_name({page})));
});
}
const ix = await InputUiUtil.pGetUserEnum({
values: values,
});
if (ix == null) return;
const ixBase = ix % 3;
if (ixdPFnConfirms[ix] && !(await ixdPFnConfirms[ix]())) return;
switch (ixBase) {
case 0: return this._pHandleClick_loadSublist_currentPinned({pFnOnSelect, page, ...ixdOtherOpts[ix]});
case 1: return this._pHandleClick_loadSublist_savedPinned({pFnOnSelect, optsSaveManager, page, ...ixdOtherOpts[ix]});
case 2: return this._pHandleClick_loadSublist_file({pFnOnSelect, page, ...ixdOtherOpts[ix]});
default: throw new Error(`Unhandled!`);
}
}
}
class _LegacyPersistedStateMigrator {
constructor () { this._legacyMigrations = []; }
registerLegacyMigration (pFnMigrate) { this._legacyMigrations.push(pFnMigrate); }
async pApplyLegacyMigrations (stored) {
const results = await this._legacyMigrations.pSerialAwaitMap(pFn => pFn(stored));
return results.some(Boolean); // Run all migrations; check if any were applied
}
}
class SublistPersistor {
static _STORAGE_KEY_SUBLIST = "sublist";
static _LEGACY_MIGRATOR = new _LegacyPersistedStateMigrator();
constructor ({page = null} = {}) {
this._page = page || UrlUtil.getCurrentPage();
}
async pGetStateFromStorage () {
let stored = await StorageUtil.pGetForPage(this.constructor._STORAGE_KEY_SUBLIST, {page: this._page});
stored = stored || {};
const isMigration = await this.constructor._LEGACY_MIGRATOR.pApplyLegacyMigrations(stored);
if (isMigration) await StorageUtil.pSetForPage(this.constructor._STORAGE_KEY_SUBLIST, stored, {page: this._page});
return stored;
}
async pDoSaveStateToStorage ({exportableSublist} = {}) {
await StorageUtil.pSetForPage(this.constructor._STORAGE_KEY_SUBLIST, exportableSublist, {page: this._page});
}
async pDoRemoveStateFromStorage () {
await StorageUtil.pRemoveForPage(this.constructor._STORAGE_KEY_SUBLIST, {page: this._page});
}
}
class SaveManager extends BaseComponent {
static _STORAGE_KEY_SAVES = "listSaveManager";
static _LEGACY_MIGRATOR = new _LegacyPersistedStateMigrator();
constructor ({isReadOnlyUi = false, isReferencable = false, page = null} = {}) {
super();
this._page = page || UrlUtil.getCurrentPage();
this._isReferencable = !!isReferencable;
this._isReadOnlyUi = !!isReadOnlyUi;
this._pDoSaveStateToStorageDebounced = MiscUtil.debounce(
this.pDoSaveStateToStorage.bind(this),
50,
);
}
// region Persistent state
async pMutStateFromStorage () {
let stored = await StorageUtil.pGetForPage(this.constructor._STORAGE_KEY_SAVES, {page: this._page});
stored = stored || this._getDefaultState();
const isMigration = await this.constructor._LEGACY_MIGRATOR.pApplyLegacyMigrations(stored);
if (isMigration) await StorageUtil.pSetForPage(this.constructor._STORAGE_KEY_SAVES, stored, {page: this._page});
this.setBaseSaveableStateFrom(stored);
}
async pDoSaveStateToStorage () {
await StorageUtil.pSetForPage(this.constructor._STORAGE_KEY_SAVES, this.getBaseSaveableState(), {page: this._page});
}
async pDoRemoveStateFromStorage () {
await StorageUtil.pRemoveForPage(this.constructor._STORAGE_KEY_SAVES, {page: this._page});
}
// endregion
_getActiveSave () { return this._state.saves.find(it => it.id === this._state.activeId); }
/** Note that the "-or-create" should never be required, but ensures we don't get into a bad state. */
_getOrCreateActiveSave () {
const save = this._getActiveSave();
if (save) return save;
this._doNew();
return this._getActiveSave();
}
mutSaveableData ({exportedSublist}) {
const save = this._getActiveSave();
if (!save) return;
["name", "saveId"]
.forEach(prop => {
if (save.entity[prop] != null) exportedSublist[prop] = save.entity[prop];
});
}
async pDoNew (exportedSublist = null) {
const isWarnUnsaved = this._isWarnUnsavedChanges(exportedSublist);
if (
isWarnUnsaved
&& !await InputUiUtil.pGetUserBoolean({title: "Discard Unsaved Changes", htmlDescription: `You have unsaved changes.
Are you sure you want to create a new list, discarding these changes?`, textYes: "Yes", textNo: "Cancel"})
) return false;
if (
// region These are mutually exclusive
!isWarnUnsaved
&& this._isWarnNeverSaved(exportedSublist)
// endregion
&& !await InputUiUtil.pGetUserBoolean({title: "Discard Unsaved List", htmlDescription: `Your current list has not been saved.
Are you sure you want to create a new list, discarding this one?`, textYes: "Yes", textNo: "Cancel"})
) return false;
this._doNew();
return true;
}
_doNew (nxt = null) {
nxt = nxt || this._getNewSave();
this._state.saves = [
...this._state.saves,
nxt,
];
this._state.activeId = nxt.id;
// Prune other unsaved state
this._state.saves = this._state.saves.filter(it => it.entity.manager_isSaved || it.id === nxt.id);
}
_getUsableSaves () { return this._state.saves.filter(it => it.entity.name && it.entity.manager_isSaved); }
async pDoUpdateCurrentStateFrom (exportedSublist, {isNoSave = false} = {}) {
if (!exportedSublist) return;
const activeSave = this._getOrCreateActiveSave();
Object.keys(this._getNewSave_entity()).forEach(k => activeSave.entity[k] = exportedSublist[k]);
this._triggerCollectionUpdate("saves");
if (!isNoSave) this._pDoSaveStateToStorageDebounced();
}
async pDoLoad (
{
isIncludeManagerClientState = false,
} = {},
) {
this._addHookBase("saves", this._pDoSaveStateToStorageDebounced);
const dispCaret = e_({
tag: "span",
clazz: "lst__caret lst__caret--active",
});
const doSortSaves = (isDescending) => {
this._state.saves.sort((a, b) => SortUtil.ascSortLower(
isDescending ? b.entity.name || "" : a.entity.name || "",
isDescending ? a.entity.name || "" : b.entity.name || ""),
);
this._triggerCollectionUpdate("saves");
dispCaret.toggleClass("lst__caret--reverse", !isDescending);
};
// Sort (and save) on opening
doSortSaves();
const $wrpIsReference = !this._isReferencable
? null
: $$``;
const $btnExportAll = $(``)
.click(() => {
DataUtil.userDownload(
ListUtil.getDownloadNameSaves({page: this._page}),
{saves: MiscUtil.copyFast(this._state.saves)},
{
fileType: ListUtil.getDownloadFiletypeSaves({page: this._page}),
},
);
});
const $btnImportAll = this._isReadOnlyUi
? null
: $(``)
.click(async () => {
const {jsons, errors} = await InputUiUtil.pGetUserUploadJson({
expectedFileTypes: [ListUtil.getDownloadFiletypeSaves({page: this._page})],
});
DataUtil.doHandleFileLoadErrorsGeneric(errors);
if (!jsons?.length) return;
const json = jsons[0];
if (!json.saves) return;
const nxt = {saves: json.saves};
if (!json.saves.some(it => it.id === this._state.activeId)) {
nxt.activeId = null;
}
this._proxyAssignSimple("state", nxt);
});
const $titleSplit = $$`