mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
v1.209.0
This commit is contained in:
1244
js/utils-brew/utils-brew-base.js
Normal file
1244
js/utils-brew/utils-brew-base.js
Normal file
File diff suppressed because it is too large
Load Diff
2
js/utils-brew/utils-brew-constants.js
Normal file
2
js/utils-brew/utils-brew-constants.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const SOURCE_UNKNOWN_FULL = "(Unknown)";
|
||||
export const SOURCE_UNKNOWN_ABBREVIATION = "(UNK)";
|
||||
7
js/utils-brew/utils-brew-helpers.js
Normal file
7
js/utils-brew/utils-brew-helpers.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export class BrewUtilShared {
|
||||
/** Prevent any injection shenanigans */
|
||||
static getValidColor (color, {isExtended = false} = {}) {
|
||||
if (isExtended) return color.replace(/[^-a-zA-Z\d]/g, "");
|
||||
return color.replace(/[^a-fA-F\d]/g, "").slice(0, 8);
|
||||
}
|
||||
}
|
||||
263
js/utils-brew/utils-brew-impl-brew.js
Normal file
263
js/utils-brew/utils-brew-impl-brew.js
Normal file
@@ -0,0 +1,263 @@
|
||||
import {BrewUtil2Base} from "./utils-brew-base.js";
|
||||
import {BrewDoc} from "./utils-brew-models.js";
|
||||
|
||||
export class BrewUtil2_ extends BrewUtil2Base {
|
||||
_STORAGE_KEY_LEGACY = "HOMEBREW_STORAGE";
|
||||
_STORAGE_KEY_LEGACY_META = "HOMEBREW_META_STORAGE";
|
||||
|
||||
// Keep these distinct from the OG brew key, so users can recover their old brew if required.
|
||||
_STORAGE_KEY = "HOMEBREW_2_STORAGE";
|
||||
_STORAGE_KEY_META = "HOMEBREW_2_STORAGE_METAS";
|
||||
|
||||
_STORAGE_KEY_CUSTOM_URL = "HOMEBREW_CUSTOM_REPO_URL";
|
||||
_STORAGE_KEY_MIGRATION_VERSION = "HOMEBREW_2_STORAGE_MIGRATION";
|
||||
|
||||
_VERSION = 2;
|
||||
|
||||
_PATH_LOCAL_DIR = "homebrew";
|
||||
_PATH_LOCAL_INDEX = VeCt.JSON_BREW_INDEX;
|
||||
|
||||
IS_EDITABLE = true;
|
||||
PAGE_MANAGE = UrlUtil.PG_MANAGE_BREW;
|
||||
URL_REPO_DEFAULT = VeCt.URL_BREW;
|
||||
URL_REPO_ROOT_DEFAULT = VeCt.URL_ROOT_BREW;
|
||||
DISPLAY_NAME = "homebrew";
|
||||
DISPLAY_NAME_PLURAL = "homebrews";
|
||||
DEFAULT_AUTHOR = "";
|
||||
STYLE_BTN = "btn-info";
|
||||
IS_PREFER_DATE_ADDED = true;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pInit_doBindDragDrop () {
|
||||
document.body.addEventListener("drop", async evt => {
|
||||
if (EventUtil.isInInput(evt)) return;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
|
||||
const files = evt.dataTransfer?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
const pFiles = [...files].map((file, i) => {
|
||||
if (!/\.json$/i.test(file.name)) return null;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(reader.result);
|
||||
} catch (ignored) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
resolve({name: file.name, json});
|
||||
};
|
||||
|
||||
reader.readAsText(files[i]);
|
||||
});
|
||||
});
|
||||
|
||||
const fileMetas = (await Promise.allSettled(pFiles))
|
||||
.filter(({status}) => status === "fulfilled")
|
||||
.map(({value}) => value)
|
||||
.filter(Boolean);
|
||||
|
||||
await this.pAddBrewsFromFiles(fileMetas);
|
||||
|
||||
if (this.isReloadRequired()) this.doLocationReload();
|
||||
});
|
||||
|
||||
document.body.addEventListener("dragover", evt => {
|
||||
if (EventUtil.isInInput(evt)) return;
|
||||
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pGetSourceIndex (urlRoot) { return DataUtil.brew.pLoadSourceIndex(urlRoot); }
|
||||
|
||||
getFileUrl (path, urlRoot) { return DataUtil.brew.getFileUrl(path, urlRoot); }
|
||||
|
||||
pLoadTimestamps (urlRoot) { return DataUtil.brew.pLoadTimestamps(urlRoot); }
|
||||
|
||||
pLoadPropIndex (urlRoot) { return DataUtil.brew.pLoadPropIndex(urlRoot); }
|
||||
|
||||
pLoadMetaIndex (urlRoot) { return DataUtil.brew.pLoadMetaIndex(urlRoot); }
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// region Editable
|
||||
async pGetEditableBrewDoc () {
|
||||
return this._findEditableBrewDoc({brewRaw: await this._pGetBrewRaw()});
|
||||
}
|
||||
|
||||
_findEditableBrewDoc ({brewRaw}) {
|
||||
return brewRaw.find(it => it.head.isEditable);
|
||||
}
|
||||
|
||||
async pGetOrCreateEditableBrewDoc () {
|
||||
const existing = await this.pGetEditableBrewDoc();
|
||||
if (existing) return existing;
|
||||
|
||||
const brew = this._getNewEditableBrewDoc();
|
||||
const brews = [...MiscUtil.copyFast(await this._pGetBrewRaw()), brew];
|
||||
await this.pSetBrew(brews);
|
||||
|
||||
return brew;
|
||||
}
|
||||
|
||||
async pSetEditableBrewDoc (brew) {
|
||||
if (!brew?.head?.docIdLocal || !brew?.body) throw new Error(`Invalid editable brew document!`); // Sanity check
|
||||
await this.pUpdateBrew(brew);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param prop
|
||||
* @param uniqueId
|
||||
* @param isDuplicate If the entity should be a duplicate, i.e. have a new `uniqueId`.
|
||||
*/
|
||||
async pGetEditableBrewEntity (prop, uniqueId, {isDuplicate = false} = {}) {
|
||||
if (!uniqueId) throw new Error(`A "uniqueId" must be provided!`);
|
||||
|
||||
const brew = await this.pGetOrCreateEditableBrewDoc();
|
||||
|
||||
const out = (brew.body?.[prop] || []).find(it => it.uniqueId === uniqueId);
|
||||
if (!out || !isDuplicate) return out;
|
||||
|
||||
if (isDuplicate) out.uniqueId = CryptUtil.uid();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async pPersistEditableBrewEntity (prop, ent) {
|
||||
if (!ent.uniqueId) throw new Error(`Entity did not have a "uniqueId"!`);
|
||||
|
||||
const brew = await this.pGetOrCreateEditableBrewDoc();
|
||||
|
||||
const ixExisting = (brew.body?.[prop] || []).findIndex(it => it.uniqueId === ent.uniqueId);
|
||||
if (!~ixExisting) {
|
||||
const nxt = MiscUtil.copyFast(brew);
|
||||
MiscUtil.getOrSet(nxt.body, prop, []).push(ent);
|
||||
|
||||
await this.pUpdateBrew(nxt);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nxt = MiscUtil.copyFast(brew);
|
||||
nxt.body[prop][ixExisting] = ent;
|
||||
|
||||
await this.pUpdateBrew(nxt);
|
||||
}
|
||||
|
||||
async pRemoveEditableBrewEntity (prop, uniqueId) {
|
||||
if (!uniqueId) throw new Error(`A "uniqueId" must be provided!`);
|
||||
|
||||
const brew = await this.pGetOrCreateEditableBrewDoc();
|
||||
|
||||
if (!brew.body?.[prop]?.length) return;
|
||||
|
||||
const nxt = MiscUtil.copyFast(brew);
|
||||
nxt.body[prop] = nxt.body[prop].filter(it => it.uniqueId !== uniqueId);
|
||||
|
||||
if (nxt.body[prop].length === brew.body[prop]) return; // Silently allow no-op deletes
|
||||
|
||||
await this.pUpdateBrew(nxt);
|
||||
}
|
||||
|
||||
async pAddSource (sourceObj) {
|
||||
const existing = await this.pGetEditableBrewDoc();
|
||||
|
||||
if (existing) {
|
||||
const nxt = MiscUtil.copyFast(existing);
|
||||
const sources = MiscUtil.getOrSet(nxt.body, "_meta", "sources", []);
|
||||
sources.push(sourceObj);
|
||||
|
||||
await this.pUpdateBrew(nxt);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const json = {_meta: {sources: [sourceObj]}};
|
||||
const brew = this._getBrewDoc({json, isEditable: true});
|
||||
const brews = [...MiscUtil.copyFast(await this._pGetBrewRaw()), brew];
|
||||
await this.pSetBrew(brews);
|
||||
}
|
||||
|
||||
async pEditSource (sourceObj) {
|
||||
const existing = await this.pGetEditableBrewDoc();
|
||||
if (!existing) throw new Error(`Editable brew document does not exist!`);
|
||||
|
||||
const nxt = MiscUtil.copyFast(existing);
|
||||
const sources = MiscUtil.get(nxt.body, "_meta", "sources");
|
||||
if (!sources) throw new Error(`Source "${sourceObj.json}" does not exist in editable brew document!`);
|
||||
|
||||
const existingSourceObj = sources.find(it => it.json === sourceObj.json);
|
||||
if (!existingSourceObj) throw new Error(`Source "${sourceObj.json}" does not exist in editable brew document!`);
|
||||
Object.assign(existingSourceObj, sourceObj);
|
||||
|
||||
await this.pUpdateBrew(nxt);
|
||||
}
|
||||
|
||||
async pIsEditableSourceJson (sourceJson) {
|
||||
const brew = await this.pGetEditableBrewDoc();
|
||||
if (!brew) return false;
|
||||
|
||||
const sources = MiscUtil.get(brew.body, "_meta", "sources") || [];
|
||||
return sources.some(it => it.json === sourceJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the brews containing a given source to the editable document. If a brew cannot be moved to the editable
|
||||
* document, copy the source to the editable document instead.
|
||||
*/
|
||||
async pMoveOrCopyToEditableBySourceJson (sourceJson) {
|
||||
if (await this.pIsEditableSourceJson(sourceJson)) return;
|
||||
|
||||
// Fetch all candidate brews
|
||||
const brews = (await this._pGetBrewRaw()).filter(brew => (brew.body._meta?.sources || []).some(src => src.json === sourceJson));
|
||||
const brewsLocal = (await this._pGetBrew_pGetLocalBrew()).filter(brew => (brew.body._meta?.sources || []).some(src => src.json === sourceJson));
|
||||
|
||||
// Arbitrarily select one, preferring non-local
|
||||
let brew = brews.find(brew => BrewDoc.isOperationPermitted_moveToEditable({brew}));
|
||||
if (!brew) brew = brewsLocal.find(brew => BrewDoc.isOperationPermitted_moveToEditable({brew, isAllowLocal: true}));
|
||||
|
||||
if (!brew) return;
|
||||
|
||||
if (brew.head.isLocal) return this.pCopyToEditable({brews: [brew]});
|
||||
|
||||
return this.pMoveToEditable({brews: [brew]});
|
||||
}
|
||||
|
||||
async pMoveToEditable ({brews}) {
|
||||
const out = await this.pCopyToEditable({brews});
|
||||
await this.pDeleteBrews(brews);
|
||||
return out;
|
||||
}
|
||||
|
||||
async pCopyToEditable ({brews}) {
|
||||
const brewEditable = await this.pGetOrCreateEditableBrewDoc();
|
||||
|
||||
const cpyBrewEditableDoc = BrewDoc.fromObject(brewEditable, {isCopy: true});
|
||||
brews.forEach((brew, i) => cpyBrewEditableDoc.mutMerge({json: brew.body, isLazy: i !== brews.length - 1}));
|
||||
|
||||
await this.pSetEditableBrewDoc(cpyBrewEditableDoc.toObject());
|
||||
|
||||
return cpyBrewEditableDoc;
|
||||
}
|
||||
|
||||
async pHasEditableSourceJson () {
|
||||
const brewsStored = await this._pGetBrewRaw();
|
||||
if (!brewsStored?.length) return false;
|
||||
|
||||
return brewsStored
|
||||
.map(brew => BrewDoc.fromObject(brew))
|
||||
.some(brew => brew.head.isEditable && !brew.isEmpty());
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
63
js/utils-brew/utils-brew-impl-prerelease.js
Normal file
63
js/utils-brew/utils-brew-impl-prerelease.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import {BrewUtil2Base} from "./utils-brew-base.js";
|
||||
|
||||
export class PrereleaseUtil_ extends BrewUtil2Base {
|
||||
_STORAGE_KEY_LEGACY = null;
|
||||
_STORAGE_KEY_LEGACY_META = null;
|
||||
|
||||
_STORAGE_KEY = "PRERELEASE_STORAGE";
|
||||
_STORAGE_KEY_META = "PRERELEASE_META_STORAGE";
|
||||
|
||||
_STORAGE_KEY_CUSTOM_URL = "PRERELEASE_CUSTOM_REPO_URL";
|
||||
_STORAGE_KEY_MIGRATION_VERSION = "PRERELEASE_STORAGE_MIGRATION";
|
||||
|
||||
_PATH_LOCAL_DIR = "prerelease";
|
||||
_PATH_LOCAL_INDEX = VeCt.JSON_PRERELEASE_INDEX;
|
||||
|
||||
_VERSION = 1;
|
||||
|
||||
IS_EDITABLE = false;
|
||||
PAGE_MANAGE = UrlUtil.PG_MANAGE_PRERELEASE;
|
||||
URL_REPO_DEFAULT = VeCt.URL_PRERELEASE;
|
||||
URL_REPO_ROOT_DEFAULT = VeCt.URL_ROOT_PRERELEASE;
|
||||
DISPLAY_NAME = "prerelease content";
|
||||
DISPLAY_NAME_PLURAL = "prereleases";
|
||||
DEFAULT_AUTHOR = "Wizards of the Coast";
|
||||
STYLE_BTN = "btn-primary";
|
||||
IS_PREFER_DATE_ADDED = false;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_pInit_doBindDragDrop () { /* No-op */ }
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pGetSourceIndex (urlRoot) { return DataUtil.prerelease.pLoadSourceIndex(urlRoot); }
|
||||
|
||||
getFileUrl (path, urlRoot) { return DataUtil.prerelease.getFileUrl(path, urlRoot); }
|
||||
|
||||
pLoadTimestamps (urlRoot) { return DataUtil.prerelease.pLoadTimestamps(urlRoot); }
|
||||
|
||||
pLoadPropIndex (urlRoot) { return DataUtil.prerelease.pLoadPropIndex(urlRoot); }
|
||||
|
||||
pLoadMetaIndex (urlRoot) { return DataUtil.prerelease.pLoadMetaIndex(urlRoot); }
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// region Editable
|
||||
|
||||
pGetEditableBrewDoc (brew) { return super.pGetEditableBrewDoc(brew); }
|
||||
pGetOrCreateEditableBrewDoc () { return super.pGetOrCreateEditableBrewDoc(); }
|
||||
pSetEditableBrewDoc () { return super.pSetEditableBrewDoc(); }
|
||||
pGetEditableBrewEntity (prop, uniqueId, {isDuplicate = false} = {}) { return super.pGetEditableBrewEntity(prop, uniqueId, {isDuplicate}); }
|
||||
pPersistEditableBrewEntity (prop, ent) { return super.pPersistEditableBrewEntity(prop, ent); }
|
||||
pRemoveEditableBrewEntity (prop, uniqueId) { return super.pRemoveEditableBrewEntity(prop, uniqueId); }
|
||||
pAddSource (sourceObj) { return super.pAddSource(sourceObj); }
|
||||
pEditSource (sourceObj) { return super.pEditSource(sourceObj); }
|
||||
pIsEditableSourceJson (sourceJson) { return super.pIsEditableSourceJson(sourceJson); }
|
||||
pMoveOrCopyToEditableBySourceJson (sourceJson) { return super.pMoveOrCopyToEditableBySourceJson(sourceJson); }
|
||||
pMoveToEditable ({brews}) { return super.pMoveToEditable({brews}); }
|
||||
pCopyToEditable ({brews}) { return super.pCopyToEditable({brews}); }
|
||||
async pHasEditableSourceJson () { return false; }
|
||||
|
||||
// endregion
|
||||
}
|
||||
261
js/utils-brew/utils-brew-models.js
Normal file
261
js/utils-brew/utils-brew-models.js
Normal file
@@ -0,0 +1,261 @@
|
||||
export class BrewDoc {
|
||||
// Things which are stored in "_meta", but are "content metadata" rather than "file metadata."
|
||||
static _META_KEYS_CONTENT_METADATA__OBJECT = [
|
||||
"skills",
|
||||
"senses",
|
||||
"spellSchools",
|
||||
"spellDistanceUnits",
|
||||
"optionalFeatureTypes",
|
||||
"psionicTypes",
|
||||
"currencyConversions",
|
||||
];
|
||||
|
||||
constructor (opts) {
|
||||
opts = opts || {};
|
||||
this.head = opts.head;
|
||||
this.body = opts.body;
|
||||
}
|
||||
|
||||
toObject () { return MiscUtil.copyFast({...this}); }
|
||||
|
||||
static fromValues ({head, body}) {
|
||||
return new this({
|
||||
head: _BrewDocHead.fromValues(head),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
static fromObject (obj, opts = {}) {
|
||||
const {isCopy = false} = opts;
|
||||
return new this({
|
||||
head: _BrewDocHead.fromObject(obj.head, opts),
|
||||
body: isCopy ? MiscUtil.copyFast(obj.body) : obj.body,
|
||||
});
|
||||
}
|
||||
|
||||
mutUpdate ({json}) {
|
||||
this.body = json;
|
||||
this.head.mutUpdate({json, body: this.body});
|
||||
return this;
|
||||
}
|
||||
|
||||
isEmpty () {
|
||||
if (
|
||||
Object.entries(this.body)
|
||||
.some(([k, v]) => {
|
||||
if (!(v instanceof Array)) return false;
|
||||
if (k === "_meta" || k === "_test") return false;
|
||||
return !!v.length;
|
||||
})
|
||||
) return false;
|
||||
|
||||
if (!this.body._meta) return false;
|
||||
|
||||
if (
|
||||
this.constructor._META_KEYS_CONTENT_METADATA__OBJECT
|
||||
.some(k => !!Object.keys(this.body._meta[k] || {}).length)
|
||||
) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// region Conditions
|
||||
static isOperationPermitted_moveToEditable ({brew, isAllowLocal = false} = {}) {
|
||||
return !brew.head.isEditable
|
||||
&& (isAllowLocal || !brew.head.isLocal);
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Merging
|
||||
mutMerge ({json, isLazy = false}) {
|
||||
this.body = this.constructor.mergeObjects({isCopy: !isLazy, isMutMakeCompatible: false}, this.body, json);
|
||||
this.head.mutMerge({json, body: this.body, isLazy});
|
||||
return this;
|
||||
}
|
||||
|
||||
static mergeObjects ({isCopy = true, isMutMakeCompatible = true} = {}, ...jsons) {
|
||||
const out = {};
|
||||
|
||||
jsons.forEach(json => {
|
||||
json = isCopy ? MiscUtil.copyFast(json) : json;
|
||||
|
||||
if (isMutMakeCompatible) this._mergeObjects_mutMakeCompatible(json);
|
||||
|
||||
Object.entries(json)
|
||||
.forEach(([prop, val]) => {
|
||||
switch (prop) {
|
||||
case "_meta": return this._mergeObjects_key__meta({out, prop, val});
|
||||
case "_test": return; // ignore; used for static testing
|
||||
default: return this._mergeObjects_default({out, prop, val});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static _META_KEYS_MERGEABLE_OBJECTS = [
|
||||
...this._META_KEYS_CONTENT_METADATA__OBJECT,
|
||||
];
|
||||
|
||||
static _META_KEYS_MERGEABLE_SPECIAL = {
|
||||
"dateAdded": (a, b) => a != null && b != null ? Math.min(a, b) : a ?? b,
|
||||
"dateLastModified": (a, b) => a != null && b != null ? Math.max(a, b) : a ?? b,
|
||||
|
||||
"dependencies": (a, b) => this._metaMerge_dependenciesIncludes(a, b),
|
||||
"includes": (a, b) => this._metaMerge_dependenciesIncludes(a, b),
|
||||
"internalCopies": (a, b) => [...(a || []), ...(b || [])].unique(),
|
||||
|
||||
"otherSources": (a, b) => this._metaMerge_otherSources(a, b),
|
||||
|
||||
"status": (a, b) => this._metaMerge_status(a, b),
|
||||
};
|
||||
|
||||
static _metaMerge_dependenciesIncludes (a, b) {
|
||||
if (a != null && b != null) {
|
||||
Object.entries(b)
|
||||
.forEach(([prop, arr]) => a[prop] = [...(a[prop] || []), ...arr].unique());
|
||||
return a;
|
||||
}
|
||||
|
||||
return a ?? b;
|
||||
}
|
||||
|
||||
static _metaMerge_otherSources (a, b) {
|
||||
if (a != null && b != null) {
|
||||
// Note that this can clobber the values in the mapping, but we don't really care since they're not used.
|
||||
Object.entries(b)
|
||||
.forEach(([prop, obj]) => a[prop] = Object.assign(a[prop] || {}, obj));
|
||||
return a;
|
||||
}
|
||||
|
||||
return a ?? b;
|
||||
}
|
||||
|
||||
static _META_MERGE__STATUS_PRECEDENCE = [
|
||||
"invalid",
|
||||
"deprecated",
|
||||
"wip",
|
||||
"ready",
|
||||
];
|
||||
|
||||
static _metaMerge_status (a, b) {
|
||||
return [a || "ready", b || "ready"]
|
||||
.sort((a, b) => this._META_MERGE__STATUS_PRECEDENCE.indexOf(a) - this._META_MERGE__STATUS_PRECEDENCE.indexOf(b))[0];
|
||||
}
|
||||
|
||||
static _mergeObjects_key__meta ({out, val}) {
|
||||
out._meta = out._meta || {};
|
||||
|
||||
out._meta.sources = [...(out._meta.sources || []), ...(val.sources || [])];
|
||||
|
||||
Object.entries(val)
|
||||
.forEach(([metaProp, metaVal]) => {
|
||||
if (this._META_KEYS_MERGEABLE_SPECIAL[metaProp]) {
|
||||
out._meta[metaProp] = this._META_KEYS_MERGEABLE_SPECIAL[metaProp](out._meta[metaProp], metaVal);
|
||||
return;
|
||||
}
|
||||
if (!this._META_KEYS_MERGEABLE_OBJECTS.includes(metaProp)) return;
|
||||
Object.assign(out._meta[metaProp] = out._meta[metaProp] || {}, metaVal);
|
||||
});
|
||||
}
|
||||
|
||||
static _mergeObjects_default ({out, prop, val}) {
|
||||
// If we cannot merge a prop, use the first value found for it, as a best-effort fallback
|
||||
if (!(val instanceof Array)) return out[prop] === undefined ? out[prop] = val : null;
|
||||
|
||||
out[prop] = [...out[prop] || [], ...val];
|
||||
}
|
||||
|
||||
static _mergeObjects_mutMakeCompatible (json) {
|
||||
// region Item
|
||||
if (json.variant) {
|
||||
// 2022-07-09
|
||||
json.magicvariant = json.variant;
|
||||
delete json.variant;
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Race
|
||||
if (json.subrace) {
|
||||
json.subrace.forEach(sr => {
|
||||
if (!sr.race) return;
|
||||
sr.raceName = sr.race.name;
|
||||
sr.raceSource = sr.race.source || sr.source || Parser.SRC_PHB;
|
||||
});
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Creature (monster)
|
||||
if (json.monster) {
|
||||
json.monster.forEach(mon => {
|
||||
// 2022-03-22
|
||||
if (typeof mon.size === "string") mon.size = [mon.size];
|
||||
|
||||
// 2022=05-29
|
||||
if (mon.summonedBySpell && !mon.summonedBySpellLevel) mon.summonedBySpellLevel = 1;
|
||||
});
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Object
|
||||
if (json.object) {
|
||||
json.object.forEach(obj => {
|
||||
// 2023-10-07
|
||||
if (typeof obj.size === "string") obj.size = [obj.size];
|
||||
});
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
class _BrewDocHead {
|
||||
constructor (opts) {
|
||||
opts = opts || {};
|
||||
|
||||
this.docIdLocal = opts.docIdLocal;
|
||||
this.timeAdded = opts.timeAdded;
|
||||
this.checksum = opts.checksum;
|
||||
this.url = opts.url;
|
||||
this.filename = opts.filename;
|
||||
this.isLocal = opts.isLocal;
|
||||
this.isEditable = opts.isEditable;
|
||||
}
|
||||
|
||||
toObject () { return MiscUtil.copyFast({...this}); }
|
||||
|
||||
static fromValues (
|
||||
{
|
||||
json,
|
||||
url = null,
|
||||
filename = null,
|
||||
isLocal = false,
|
||||
isEditable = false,
|
||||
},
|
||||
) {
|
||||
return new this({
|
||||
docIdLocal: CryptUtil.uid(),
|
||||
timeAdded: Date.now(),
|
||||
checksum: CryptUtil.md5(JSON.stringify(json)),
|
||||
url: url,
|
||||
filename: filename,
|
||||
isLocal: isLocal,
|
||||
isEditable: isEditable,
|
||||
});
|
||||
}
|
||||
|
||||
static fromObject (obj, {isCopy = false} = {}) {
|
||||
return new this(isCopy ? MiscUtil.copyFast(obj) : obj);
|
||||
}
|
||||
|
||||
mutUpdate ({json}) {
|
||||
this.checksum = CryptUtil.md5(JSON.stringify(json));
|
||||
return this;
|
||||
}
|
||||
|
||||
mutMerge ({json, body, isLazy}) {
|
||||
if (!isLazy) this.checksum = CryptUtil.md5(JSON.stringify(body ?? json));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
488
js/utils-brew/utils-brew-ui-get.js
Normal file
488
js/utils-brew/utils-brew-ui-get.js
Normal file
@@ -0,0 +1,488 @@
|
||||
import {EVNT_VALCHANGE} from "../filter/filter-constants.js";
|
||||
|
||||
export class GetBrewUi {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.pageFilter = null;
|
||||
this.list = null;
|
||||
this.listSelectClickHandler = null;
|
||||
this.cbAll = null;
|
||||
}
|
||||
};
|
||||
|
||||
static _TypeFilter = class extends Filter {
|
||||
constructor ({brewUtil}) {
|
||||
const pageProps = brewUtil.getPageProps({fallback: ["*"]});
|
||||
super({
|
||||
header: "Category",
|
||||
items: [],
|
||||
displayFn: brewUtil.getPropDisplayName.bind(brewUtil),
|
||||
selFn: prop => pageProps.includes("*") || pageProps.includes(prop),
|
||||
isSortByDisplayItems: true,
|
||||
});
|
||||
this._brewUtil = brewUtil;
|
||||
}
|
||||
|
||||
_getHeaderControls_addExtraStateBtns (opts, wrpStateBtnsOuter) {
|
||||
const menu = ContextUtil.getMenu(
|
||||
this._brewUtil.getPropPages()
|
||||
.map(page => ({page, displayPage: UrlUtil.pageToDisplayPage(page)}))
|
||||
.sort(SortUtil.ascSortProp.bind(SortUtil, "displayPage"))
|
||||
.map(({page, displayPage}) => {
|
||||
return new ContextUtil.Action(
|
||||
displayPage,
|
||||
() => {
|
||||
const propsActive = new Set(this._brewUtil.getPageProps({page, fallback: []}));
|
||||
Object.keys(this._state).forEach(prop => this._state[prop] = propsActive.has(prop) ? 1 : 0);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const btnPage = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default w-100 btn-xs`,
|
||||
text: `Select for Page...`,
|
||||
click: evt => ContextUtil.pOpenMenu(evt, menu),
|
||||
});
|
||||
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `btn-group mr-2 w-100 ve-flex-v-center`,
|
||||
children: [
|
||||
btnPage,
|
||||
],
|
||||
}).prependTo(wrpStateBtnsOuter);
|
||||
}
|
||||
};
|
||||
|
||||
static _PageFilterGetBrew = class extends PageFilterBase {
|
||||
static _STATUS_FILTER_DEFAULT_DESELECTED = new Set(["wip", "deprecated", "invalid"]);
|
||||
|
||||
constructor ({brewUtil}) {
|
||||
super();
|
||||
|
||||
this._brewUtil = brewUtil;
|
||||
|
||||
this._typeFilter = new GetBrewUi._TypeFilter({brewUtil});
|
||||
this._statusFilter = new Filter({
|
||||
header: "Status",
|
||||
items: [
|
||||
"ready",
|
||||
"wip",
|
||||
"deprecated",
|
||||
"invalid",
|
||||
],
|
||||
displayFn: StrUtil.toTitleCase,
|
||||
itemSortFn: null,
|
||||
deselFn: it => this.constructor._STATUS_FILTER_DEFAULT_DESELECTED.has(it),
|
||||
});
|
||||
this._miscFilter = new Filter({
|
||||
header: "Miscellaneous",
|
||||
items: ["Sample"],
|
||||
deselFn: it => it === "Sample",
|
||||
});
|
||||
}
|
||||
|
||||
static mutateForFilters (brewInfo) {
|
||||
brewInfo._fMisc = [];
|
||||
if (brewInfo._brewAuthor && brewInfo._brewAuthor.toLowerCase().startsWith("sample -")) brewInfo._fMisc.push("Sample");
|
||||
if (brewInfo.sources?.some(ab => ab.startsWith(Parser.SRC_UA_ONE_PREFIX))) brewInfo._fMisc.push("One D&D");
|
||||
}
|
||||
|
||||
addToFilters (it, isExcluded) {
|
||||
if (isExcluded) return;
|
||||
|
||||
this._typeFilter.addItem(it.props);
|
||||
this._miscFilter.addItem(it._fMisc);
|
||||
}
|
||||
|
||||
async _pPopulateBoxOptions (opts) {
|
||||
opts.filters = [
|
||||
this._typeFilter,
|
||||
this._statusFilter,
|
||||
this._miscFilter,
|
||||
];
|
||||
}
|
||||
|
||||
toDisplay (values, it) {
|
||||
return this._filterBox.toDisplay(
|
||||
values,
|
||||
it.props,
|
||||
it._brewStatus,
|
||||
it._fMisc,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static async pDoGetBrew ({brewUtil, isModal: isParentModal = false} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ui = new this({brewUtil, isModal: true});
|
||||
const rdState = new this._RenderState();
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
isHeight100: true,
|
||||
title: `Get ${brewUtil.DISPLAY_NAME.toTitleCase()}`,
|
||||
isUncappedHeight: true,
|
||||
isWidth100: true,
|
||||
overlayColor: isParentModal ? "transparent" : undefined,
|
||||
isHeaderBorder: true,
|
||||
cbClose: async () => {
|
||||
await ui.pHandlePreCloseModal({rdState});
|
||||
resolve([...ui._brewsLoaded]);
|
||||
},
|
||||
});
|
||||
ui.pInit()
|
||||
.then(() => ui.pRender($modalInner, {rdState}))
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
_sortUrlList (a, b, o) {
|
||||
a = this._dataList[a.ix];
|
||||
b = this._dataList[b.ix];
|
||||
|
||||
switch (o.sortBy) {
|
||||
case "name": return this.constructor._sortUrlList_byName(a, b);
|
||||
case "author": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSortLower, "_brewAuthor");
|
||||
case "category": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSortLower, "_brewPropDisplayName");
|
||||
case "added": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewAdded");
|
||||
case "modified": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewModified");
|
||||
case "published": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewPublished");
|
||||
default: throw new Error(`No sort order defined for property "${o.sortBy}"`);
|
||||
}
|
||||
}
|
||||
|
||||
static _sortUrlList_byName (a, b) { return SortUtil.ascSortLower(a._brewName, b._brewName); }
|
||||
static _sortUrlList_orFallback (a, b, fn, prop) { return fn(a[prop], b[prop]) || this._sortUrlList_byName(a, b); }
|
||||
|
||||
constructor ({brewUtil, isModal} = {}) {
|
||||
this._brewUtil = brewUtil;
|
||||
this._isModal = isModal;
|
||||
|
||||
this._dataList = null;
|
||||
|
||||
this._brewsLoaded = []; // Track the brews we load during our lifetime
|
||||
}
|
||||
|
||||
async pInit () {
|
||||
this._dataList = await this._brewUtil.pGetCombinedIndexes();
|
||||
}
|
||||
|
||||
async pHandlePreCloseModal ({rdState}) {
|
||||
// region If the user has selected list items, prompt to load them before closing the modal
|
||||
const cntSel = rdState.list.items.filter(it => it.data.cbSel.checked).length;
|
||||
if (!cntSel) return;
|
||||
|
||||
const isSave = await InputUiUtil.pGetUserBoolean({
|
||||
title: `Selected ${this._brewUtil.DISPLAY_NAME}`,
|
||||
htmlDescription: `You have ${cntSel} ${cntSel === 1 ? this._brewUtil.DISPLAY_NAME : this._brewUtil.DISPLAY_NAME_PLURAL} selected which ${cntSel === 1 ? "is" : "are"} not yet loaded. Would you like to load ${cntSel === 1 ? "it" : "them"}?`,
|
||||
textYes: "Load",
|
||||
textNo: "Discard",
|
||||
});
|
||||
if (!isSave) return;
|
||||
|
||||
await this._pHandleClick_btnAddSelected({rdState});
|
||||
// endregion
|
||||
}
|
||||
|
||||
async pRender ($wrp, {rdState} = {}) {
|
||||
rdState = rdState || new this.constructor._RenderState();
|
||||
|
||||
rdState.pageFilter = new this.constructor._PageFilterGetBrew({brewUtil: this._brewUtil});
|
||||
|
||||
const $btnAddSelected = $(`<button class="btn ${this._brewUtil.STYLE_BTN} btn-sm ve-col-0-5 ve-text-center" disabled title="Add Selected"><span class="glyphicon glyphicon-save"></button>`);
|
||||
|
||||
const $wrpRows = $$`<div class="list smooth-scroll max-h-unset"><div class="lst__row ve-flex-col"><div class="lst__wrp-cells lst--border lst__row-inner ve-flex w-100"><i>Loading...</i></div></div></div>`;
|
||||
|
||||
const $btnFilter = $(`<button class="btn btn-default btn-sm">Filter</button>`);
|
||||
|
||||
const $btnToggleSummaryHidden = $(`<button class="btn btn-default" title="Toggle Filter Summary Display"><span class="glyphicon glyphicon-resize-small"></span></button>`);
|
||||
|
||||
const $iptSearch = $(`<input type="search" class="search manbrew__search form-control w-100 lst__search lst__search--no-border-h" placeholder="Find ${this._brewUtil.DISPLAY_NAME}...">`)
|
||||
.keydown(evt => this._pHandleKeydown_iptSearch(evt, rdState));
|
||||
const $dispCntVisible = $(`<div class="lst__wrp-search-visible no-events ve-flex-vh-center"></div>`);
|
||||
|
||||
rdState.cbAll = e_({
|
||||
tag: "input",
|
||||
type: "checkbox",
|
||||
});
|
||||
|
||||
const $btnReset = $(`<button class="btn btn-default btn-sm">Reset</button>`);
|
||||
|
||||
const $wrpMiniPills = $(`<div class="fltr__mini-view btn-group"></div>`);
|
||||
|
||||
const btnSortAddedPublished = this._brewUtil.IS_PREFER_DATE_ADDED
|
||||
? `<button class="ve-col-1-4 sort btn btn-default btn-xs" data-sort="added">Added</button>`
|
||||
: `<button class="ve-col-1-4 sort btn btn-default btn-xs" data-sort="published">Published</button>`;
|
||||
|
||||
const $wrpSort = $$`<div class="filtertools manbrew__filtertools btn-group input-group input-group--bottom ve-flex no-shrink">
|
||||
<label class="ve-col-0-5 pr-0 btn btn-default btn-xs ve-flex-vh-center">${rdState.cbAll}</label>
|
||||
<button class="ve-col-3-5 sort btn btn-default btn-xs" data-sort="name">Name</button>
|
||||
<button class="ve-col-3 sort btn btn-default btn-xs" data-sort="author">Author</button>
|
||||
<button class="ve-col-1-2 sort btn btn-default btn-xs" data-sort="category">Category</button>
|
||||
<button class="ve-col-1-4 sort btn btn-default btn-xs" data-sort="modified">Modified</button>
|
||||
${btnSortAddedPublished}
|
||||
<button class="sort btn btn-default btn-xs ve-grow" disabled>Source</button>
|
||||
</div>`;
|
||||
|
||||
$$($wrp)`
|
||||
<div class="mt-1"><i>A list of ${this._brewUtil.DISPLAY_NAME} available in the public repository. Click a name to load the ${this._brewUtil.DISPLAY_NAME}, or view the source directly.${this._brewUtil.IS_EDITABLE ? `<br>
|
||||
Contributions are welcome; see the <a href="${this._brewUtil.URL_REPO_DEFAULT}/blob/master/README.md" target="_blank" rel="noopener noreferrer">README</a>, or stop by our <a href="https://discord.gg/5etools" target="_blank" rel="noopener noreferrer">Discord</a>.` : ""}</i></div>
|
||||
<hr class="hr-3">
|
||||
<div class="lst__form-top">
|
||||
${$btnAddSelected}
|
||||
${$btnFilter}
|
||||
${$btnToggleSummaryHidden}
|
||||
<div class="w-100 relative">
|
||||
${$iptSearch}
|
||||
<div id="lst__search-glass" class="lst__wrp-search-glass no-events ve-flex-vh-center"><span class="glyphicon glyphicon-search"></span></div>
|
||||
${$dispCntVisible}
|
||||
</div>
|
||||
${$btnReset}
|
||||
</div>
|
||||
${$wrpMiniPills}
|
||||
${$wrpSort}
|
||||
${$wrpRows}`;
|
||||
|
||||
rdState.list = new List({
|
||||
$iptSearch,
|
||||
$wrpList: $wrpRows,
|
||||
fnSort: this._sortUrlList.bind(this),
|
||||
isUseJquery: true,
|
||||
isFuzzy: true,
|
||||
isSkipSearchKeybindingEnter: true,
|
||||
});
|
||||
|
||||
rdState.list.on("updated", () => $dispCntVisible.html(`${rdState.list.visibleItems.length}/${rdState.list.items.length}`));
|
||||
|
||||
rdState.listSelectClickHandler = new ListSelectClickHandler({list: rdState.list});
|
||||
rdState.listSelectClickHandler.bindSelectAllCheckbox($(rdState.cbAll));
|
||||
SortUtil.initBtnSortHandlers($wrpSort, rdState.list);
|
||||
|
||||
this._dataList.forEach((brewInfo, ix) => {
|
||||
const {listItem} = this._pRender_getUrlRowMeta(rdState, brewInfo, ix);
|
||||
rdState.list.addItem(listItem);
|
||||
});
|
||||
|
||||
await rdState.pageFilter.pInitFilterBox({
|
||||
$iptSearch: $iptSearch,
|
||||
$btnReset: $btnReset,
|
||||
$btnOpen: $btnFilter,
|
||||
$btnToggleSummaryHidden,
|
||||
$wrpMiniPills,
|
||||
namespace: `get-homebrew-${UrlUtil.getCurrentPage()}`,
|
||||
});
|
||||
|
||||
this._dataList.forEach(it => rdState.pageFilter.mutateAndAddToFilters(it));
|
||||
|
||||
rdState.list.init();
|
||||
|
||||
rdState.pageFilter.trimState();
|
||||
rdState.pageFilter.filterBox.render();
|
||||
|
||||
rdState.pageFilter.filterBox.on(
|
||||
EVNT_VALCHANGE,
|
||||
this._handleFilterChange.bind(this, rdState),
|
||||
);
|
||||
|
||||
this._handleFilterChange(rdState);
|
||||
|
||||
$btnAddSelected
|
||||
.prop("disabled", false)
|
||||
.click(() => this._pHandleClick_btnAddSelected({rdState}));
|
||||
|
||||
$iptSearch.focus();
|
||||
}
|
||||
|
||||
_handleFilterChange (rdState) {
|
||||
const f = rdState.pageFilter.filterBox.getValues();
|
||||
rdState.list.filter(li => rdState.pageFilter.toDisplay(f, this._dataList[li.ix]));
|
||||
}
|
||||
|
||||
_pRender_getUrlRowMeta (rdState, brewInfo, ix) {
|
||||
const epochAddedPublished = this._brewUtil.IS_PREFER_DATE_ADDED ? brewInfo._brewAdded : brewInfo._brewPublished;
|
||||
const timestampAddedPublished = epochAddedPublished
|
||||
? DatetimeUtil.getDateStr({date: new Date(epochAddedPublished * 1000), isShort: true, isPad: true})
|
||||
: "";
|
||||
const timestampModified = brewInfo._brewModified
|
||||
? DatetimeUtil.getDateStr({date: new Date(brewInfo._brewModified * 1000), isShort: true, isPad: true})
|
||||
: "";
|
||||
|
||||
const cbSel = e_({
|
||||
tag: "input",
|
||||
clazz: "no-events",
|
||||
type: "checkbox",
|
||||
});
|
||||
|
||||
const btnAdd = e_({
|
||||
tag: "span",
|
||||
clazz: `ve-col-3-5 bold manbrew__load_from_url pl-0 clickable`,
|
||||
text: brewInfo._brewName,
|
||||
click: evt => this._pHandleClick_btnGetRemote({evt, btn: btnAdd, url: brewInfo.urlDownload}),
|
||||
});
|
||||
|
||||
const eleLi = e_({
|
||||
tag: "div",
|
||||
clazz: `lst__row lst__row-inner not-clickable lst--border lst__row--focusable no-select`,
|
||||
children: [
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `lst__wrp-cells ve-flex w-100`,
|
||||
children: [
|
||||
e_({
|
||||
tag: "label",
|
||||
clazz: `ve-col-0-5 ve-flex-vh-center ve-self-flex-stretch`,
|
||||
children: [cbSel],
|
||||
}),
|
||||
btnAdd,
|
||||
e_({tag: "span", clazz: "ve-col-3", text: brewInfo._brewAuthor}),
|
||||
e_({tag: "span", clazz: "ve-col-1-2 ve-text-center mobile__text-clip-ellipsis", text: brewInfo._brewPropDisplayName, title: brewInfo._brewPropDisplayName}),
|
||||
e_({tag: "span", clazz: "ve-col-1-4 ve-text-center code", text: timestampModified}),
|
||||
e_({tag: "span", clazz: "ve-col-1-4 ve-text-center code", text: timestampAddedPublished}),
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "ve-col-1 manbrew__source ve-text-center pr-0",
|
||||
children: [
|
||||
e_({
|
||||
tag: "a",
|
||||
text: `View Raw`,
|
||||
})
|
||||
.attr("href", brewInfo.urlDownload)
|
||||
.attr("target", "_blank")
|
||||
.attr("rel", "noopener noreferrer"),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
keydown: evt => this._pHandleKeydown_row(evt, {rdState, btnAdd, url: brewInfo.urlDownload, listItem}),
|
||||
})
|
||||
.attr("tabindex", ix);
|
||||
|
||||
const listItem = new ListItem(
|
||||
ix,
|
||||
eleLi,
|
||||
brewInfo._brewName,
|
||||
{
|
||||
author: brewInfo._brewAuthor,
|
||||
// category: brewInfo._brewPropDisplayName, // Unwanted in search
|
||||
internalSources: brewInfo._brewInternalSources, // Used for search
|
||||
},
|
||||
{
|
||||
btnAdd,
|
||||
cbSel,
|
||||
pFnDoDownload: ({isLazy = false} = {}) => this._pHandleClick_btnGetRemote({btn: btnAdd, url: brewInfo.urlDownload, isLazy}),
|
||||
},
|
||||
);
|
||||
|
||||
eleLi.addEventListener("click", evt => rdState.listSelectClickHandler.handleSelectClick(listItem, evt, {isPassThroughEvents: true}));
|
||||
|
||||
return {
|
||||
listItem,
|
||||
};
|
||||
}
|
||||
|
||||
async _pHandleKeydown_iptSearch (evt, rdState) {
|
||||
switch (evt.key) {
|
||||
case "Enter": {
|
||||
const firstItem = rdState.list.visibleItems[0];
|
||||
if (!firstItem) return;
|
||||
await firstItem.data.pFnDoDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
case "ArrowDown": {
|
||||
const firstItem = rdState.list.visibleItems[0];
|
||||
if (firstItem) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
firstItem.ele.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _pHandleClick_btnAddSelected ({rdState}) {
|
||||
const listItems = rdState.list.items.filter(it => it.data.cbSel.checked);
|
||||
|
||||
if (!listItems.length) return JqueryUtil.doToast({type: "warning", content: `Please select some ${this._brewUtil.DISPLAY_NAME_PLURAL} first!`});
|
||||
|
||||
if (listItems.length > 25 && !await InputUiUtil.pGetUserBoolean({title: "Are you sure?", htmlDescription: `<div>You area about to load ${listItems.length} ${this._brewUtil.DISPLAY_NAME} files.<br>Loading large quantities of ${this._brewUtil.DISPLAY_NAME_PLURAL} can lead to performance and stability issues.</div>`, textYes: "Continue"})) return;
|
||||
|
||||
rdState.cbAll.checked = false;
|
||||
rdState.list.items.forEach(item => {
|
||||
item.data.cbSel.checked = false;
|
||||
item.ele.classList.remove("list-multi-selected");
|
||||
});
|
||||
|
||||
await Promise.allSettled(listItems.map(it => it.data.pFnDoDownload({isLazy: true})));
|
||||
const lazyDepsAdded = await this._brewUtil.pAddBrewsLazyFinalize();
|
||||
this._brewsLoaded.push(...lazyDepsAdded);
|
||||
JqueryUtil.doToast(`Finished loading selected ${this._brewUtil.DISPLAY_NAME}!`);
|
||||
}
|
||||
|
||||
async _pHandleClick_btnGetRemote ({evt, btn, url, isLazy}) {
|
||||
if (!(url || "").trim()) return JqueryUtil.doToast({type: "danger", content: `${this._brewUtil.DISPLAY_NAME.uppercaseFirst()} had no download URL!`});
|
||||
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
const cachedHtml = btn.html();
|
||||
btn.txt("Loading...").attr("disabled", true);
|
||||
const brewsAdded = await this._brewUtil.pAddBrewFromUrl(url, {isLazy});
|
||||
this._brewsLoaded.push(...brewsAdded);
|
||||
btn.txt("Done!");
|
||||
setTimeout(() => btn.html(cachedHtml).attr("disabled", false), VeCt.DUR_INLINE_NOTIFY);
|
||||
}
|
||||
|
||||
async _pHandleKeydown_row (evt, {rdState, btnAdd, url, listItem}) {
|
||||
switch (evt.key) {
|
||||
case "Enter": return this._pHandleClick_btnGetRemote({evt, btn: btnAdd, url});
|
||||
|
||||
case "ArrowUp": {
|
||||
const ixCur = rdState.list.visibleItems.indexOf(listItem);
|
||||
|
||||
if (~ixCur) {
|
||||
const prevItem = rdState.list.visibleItems[ixCur - 1];
|
||||
if (prevItem) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
prevItem.ele.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const firstItem = rdState.list.visibleItems[0];
|
||||
if (firstItem) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
firstItem.ele.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case "ArrowDown": {
|
||||
const ixCur = rdState.list.visibleItems.indexOf(listItem);
|
||||
|
||||
if (~ixCur) {
|
||||
const nxtItem = rdState.list.visibleItems[ixCur + 1];
|
||||
if (nxtItem) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
nxtItem.ele.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const lastItem = rdState.list.visibleItems.last();
|
||||
if (lastItem) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
lastItem.ele.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
531
js/utils-brew/utils-brew-ui-manage-editable-contents.js
Normal file
531
js/utils-brew/utils-brew-ui-manage-editable-contents.js
Normal file
@@ -0,0 +1,531 @@
|
||||
import {SOURCE_UNKNOWN_ABBREVIATION, SOURCE_UNKNOWN_FULL} from "./utils-brew-constants.js";
|
||||
import {EVNT_VALCHANGE} from "../filter/filter-constants.js";
|
||||
|
||||
export class ManageEditableBrewContentsUi extends BaseComponent {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.tabMetaEntities = null;
|
||||
this.tabMetaSources = null;
|
||||
|
||||
this.listEntities = null;
|
||||
this.listEntitiesSelectClickHandler = null;
|
||||
this.listSources = null;
|
||||
this.listSourcesSelectClickHandler = null;
|
||||
|
||||
this.contentEntities = null;
|
||||
this.pageFilterEntities = new ManageEditableBrewContentsUi._PageFilter();
|
||||
}
|
||||
};
|
||||
|
||||
static _PageFilter = class extends PageFilterBase {
|
||||
constructor () {
|
||||
super();
|
||||
this._categoryFilter = new Filter({header: "Category"});
|
||||
}
|
||||
|
||||
static mutateForFilters (meta) {
|
||||
const {ent, prop} = meta;
|
||||
meta._fSource = SourceUtil.getEntitySource(ent);
|
||||
meta._fCategory = ManageEditableBrewContentsUi._getDisplayProp({ent, prop});
|
||||
}
|
||||
|
||||
addToFilters (meta) {
|
||||
this._sourceFilter.addItem(meta._fSource);
|
||||
this._categoryFilter.addItem(meta._fCategory);
|
||||
}
|
||||
|
||||
async _pPopulateBoxOptions (opts) {
|
||||
opts.filters = [
|
||||
this._sourceFilter,
|
||||
this._categoryFilter,
|
||||
];
|
||||
}
|
||||
|
||||
toDisplay (values, meta) {
|
||||
return this._filterBox.toDisplay(
|
||||
values,
|
||||
meta._fSource,
|
||||
meta._fCategory,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static async pDoOpen ({brewUtil, brew, isModal: isParentModal = false}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ui = new this({brewUtil, brew, isModal: true});
|
||||
const rdState = new this._RenderState();
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
isHeight100: true,
|
||||
title: `Manage Document Contents`,
|
||||
isUncappedHeight: true,
|
||||
isWidth100: true,
|
||||
$titleSplit: $$`<div class="ve-flex-v-center btn-group">
|
||||
${ui._$getBtnDeleteSelected({rdState})}
|
||||
</div>`,
|
||||
overlayColor: isParentModal ? "transparent" : undefined,
|
||||
cbClose: () => {
|
||||
resolve(ui._getFormData());
|
||||
rdState.pageFilterEntities.filterBox.teardown();
|
||||
},
|
||||
});
|
||||
ui.pRender($modalInner, {rdState})
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
constructor ({brewUtil, brew, isModal}) {
|
||||
super();
|
||||
|
||||
TabUiUtil.decorate(this, {isInitMeta: true});
|
||||
|
||||
this._brewUtil = brewUtil;
|
||||
this._brew = MiscUtil.copyFast(brew);
|
||||
this._isModal = isModal;
|
||||
|
||||
this._isDirty = false;
|
||||
}
|
||||
|
||||
_getFormData () {
|
||||
return {
|
||||
isDirty: this._isDirty,
|
||||
brew: this._brew,
|
||||
};
|
||||
}
|
||||
|
||||
_$getBtnDeleteSelected ({rdState}) {
|
||||
return $(`<button class="btn btn-danger btn-xs">Delete Selected</button>`)
|
||||
.click(() => this._handleClick_pButtonDeleteSelected({rdState}));
|
||||
}
|
||||
|
||||
async _handleClick_pButtonDeleteSelected ({rdState}) {
|
||||
if (this._getActiveTab() === rdState.tabMetaEntities) return this._handleClick_pButtonDeleteSelected_entities({rdState});
|
||||
if (this._getActiveTab() === rdState.tabMetaSources) return this._handleClick_pButtonDeleteSelected_sources({rdState});
|
||||
// (The metadata tab does not have any selectable elements, so, no-op)
|
||||
}
|
||||
|
||||
async _handleClick_pButtonDeleteSelected_entities ({rdState}) {
|
||||
const listItemsSel = rdState.listEntities.items
|
||||
.filter(it => it.data.cbSel.checked);
|
||||
|
||||
if (!listItemsSel.length) return;
|
||||
|
||||
if (!await InputUiUtil.pGetUserBoolean({title: "Delete Entities", htmlDescription: `Are you sure you want to delete the ${listItemsSel.length === 1 ? "selected entity" : `${listItemsSel.length} selected entities`}?`, textYes: "Yes", textNo: "Cancel"})) return;
|
||||
|
||||
this._isDirty = true;
|
||||
|
||||
// Remove the array items from our copy of the brew, and remove the corresponding list items
|
||||
listItemsSel
|
||||
.forEach(li => this._doEntityListDelete({rdState, li}));
|
||||
rdState.listEntities.update();
|
||||
}
|
||||
|
||||
_doEntityListDelete ({rdState, li}) {
|
||||
const ix = this._brew.body[li.data.prop].indexOf(li.data.ent);
|
||||
if (!~ix) return;
|
||||
this._brew.body[li.data.prop].splice(ix, 1);
|
||||
if (!this._brew.body[li.data.prop].length) delete this._brew.body[li.data.prop];
|
||||
rdState.listEntities.removeItem(li);
|
||||
}
|
||||
|
||||
async _handleClick_pButtonDeleteSelected_sources ({rdState}) {
|
||||
const listItemsSel = rdState.listSources.items
|
||||
.filter(it => it.data.cbSel.checked);
|
||||
|
||||
if (!listItemsSel.length) return;
|
||||
|
||||
if (
|
||||
!await InputUiUtil.pGetUserBoolean({
|
||||
title: "Delete Sources",
|
||||
htmlDescription: `<div>Are you sure you want to delete the ${listItemsSel.length === 1 ? "selected source" : `${listItemsSel.length} selected sources`}?<br><b>This will delete all entities with ${listItemsSel.length === 1 ? "that source" : `these sources`}</b>.</div>`,
|
||||
textYes: "Yes",
|
||||
textNo: "Cancel",
|
||||
})
|
||||
) return;
|
||||
|
||||
this._isDirty = true;
|
||||
|
||||
// Remove the sources from our copy of the brew, and remove the corresponding list items
|
||||
listItemsSel
|
||||
.forEach(li => {
|
||||
const ix = this._brew.body._meta.sources.indexOf(li.data.source);
|
||||
if (!~ix) return;
|
||||
this._brew.body._meta.sources.splice(ix, 1);
|
||||
rdState.listSources.removeItem(li);
|
||||
});
|
||||
rdState.listSources.update();
|
||||
|
||||
// Remove all entities with matching sources, and remove the corresponding list items
|
||||
const sourceSetRemoved = new Set(listItemsSel.map(li => li.data.source.json));
|
||||
rdState.listEntities.visibleItems
|
||||
.forEach(li => {
|
||||
const source = SourceUtil.getEntitySource(li.data.ent);
|
||||
if (!sourceSetRemoved.has(source)) return;
|
||||
|
||||
this._doEntityListDelete({rdState, li});
|
||||
});
|
||||
rdState.listEntities.update();
|
||||
}
|
||||
|
||||
async pRender ($wrp, {rdState = null} = {}) {
|
||||
rdState = rdState || new this.constructor._RenderState();
|
||||
|
||||
const iptTabMetas = [
|
||||
new TabUiUtil.TabMeta({name: "Entities", hasBorder: true}),
|
||||
new TabUiUtil.TabMeta({name: "Metadata", hasBorder: true}),
|
||||
new TabUiUtil.TabMeta({name: "Sources", hasBorder: true}),
|
||||
];
|
||||
|
||||
const tabMetas = this._renderTabs(iptTabMetas, {$parent: $wrp});
|
||||
const [tabMetaEntities, tabMetaMetadata, tabMetaSources] = tabMetas;
|
||||
|
||||
rdState.tabMetaEntities = tabMetaEntities;
|
||||
rdState.tabMetaSources = tabMetaSources;
|
||||
|
||||
this._pRender_tabEntities({tabMeta: tabMetaEntities, rdState});
|
||||
this._pRender_tabMetadata({tabMeta: tabMetaMetadata, rdState});
|
||||
this._pRender_tabSources({tabMeta: tabMetaSources, rdState});
|
||||
}
|
||||
|
||||
_pRender_tabEntities ({tabMeta, rdState}) {
|
||||
const $btnFilter = $(`<button class="btn btn-default">Filter</button>`);
|
||||
|
||||
const $btnToggleSummaryHidden = $(`<button class="btn btn-default" title="Toggle Filter Summary Display"><span class="glyphicon glyphicon-resize-small"></span></button>`);
|
||||
|
||||
const $btnReset = $(`<button class="btn btn-default">Reset</button>`);
|
||||
|
||||
const $wrpMiniPills = $(`<div class="fltr__mini-view btn-group"></div>`);
|
||||
|
||||
const $cbAll = $(`<input type="checkbox">`);
|
||||
const $wrpRows = $$`<div class="list ve-flex-col w-100 max-h-unset"></div>`;
|
||||
const $iptSearch = $(`<input type="search" class="search manbrew__search form-control w-100 lst__search lst__search--no-border-h" placeholder="Search entries...">`);
|
||||
const $dispCntVisible = $(`<div class="lst__wrp-search-visible no-events ve-flex-vh-center"></div>`);
|
||||
const $wrpBtnsSort = $$`<div class="filtertools manbrew__filtertools input-group input-group--bottom ve-flex no-shrink">
|
||||
<label class="btn btn-default btn-xs ve-col-1 pr-0 ve-flex-vh-center">${$cbAll}</label>
|
||||
<button class="ve-col-5 sort btn btn-default btn-xs" data-sort="name">Name</button>
|
||||
<button class="ve-col-1 sort btn btn-default btn-xs" data-sort="source">Source</button>
|
||||
<button class="ve-col-5 sort btn btn-default btn-xs" data-sort="category">Category</button>
|
||||
</div>`;
|
||||
|
||||
$$(tabMeta.$wrpTab)`
|
||||
<div class="ve-flex-v-stretch input-group input-group--top no-shrink mt-1">
|
||||
${$btnFilter}
|
||||
${$btnToggleSummaryHidden}
|
||||
<div class="w-100 relative">
|
||||
${$iptSearch}
|
||||
<div id="lst__search-glass" class="lst__wrp-search-glass no-events ve-flex-vh-center"><span class="glyphicon glyphicon-search"></span></div>
|
||||
${$dispCntVisible}
|
||||
</div>
|
||||
${$btnReset}
|
||||
</div>
|
||||
|
||||
${$wrpMiniPills}
|
||||
|
||||
${$wrpBtnsSort}
|
||||
${$wrpRows}`;
|
||||
|
||||
rdState.listEntities = new List({
|
||||
$iptSearch,
|
||||
$wrpList: $wrpRows,
|
||||
fnSort: SortUtil.listSort,
|
||||
});
|
||||
|
||||
rdState.listEntities.on("updated", () => $dispCntVisible.html(`${rdState.listEntities.visibleItems.length}/${rdState.listEntities.items.length}`));
|
||||
|
||||
rdState.listEntitiesSelectClickHandler = new ListSelectClickHandler({list: rdState.listEntities});
|
||||
rdState.listEntitiesSelectClickHandler.bindSelectAllCheckbox($cbAll);
|
||||
SortUtil.initBtnSortHandlers($wrpBtnsSort, rdState.listEntities);
|
||||
|
||||
let ixParent = 0;
|
||||
rdState.contentEntities = Object.entries(this._brew.body)
|
||||
.filter(([, v]) => v instanceof Array && v.length)
|
||||
.map(([prop, arr]) => arr.map(ent => ({ent, prop, ixParent: ixParent++})))
|
||||
.flat();
|
||||
|
||||
rdState.contentEntities.forEach(({ent, prop, ixParent}) => {
|
||||
const {listItem} = this._pRender_getEntityRowMeta({rdState, prop, ent, ixParent});
|
||||
rdState.listEntities.addItem(listItem);
|
||||
});
|
||||
|
||||
rdState.pageFilterEntities.pInitFilterBox({
|
||||
$iptSearch: $iptSearch,
|
||||
$btnReset: $btnReset,
|
||||
$btnOpen: $btnFilter,
|
||||
$btnToggleSummaryHidden: $btnToggleSummaryHidden,
|
||||
$wrpMiniPills: $wrpMiniPills,
|
||||
namespace: `${this.constructor.name}__tabEntities`,
|
||||
}).then(async () => {
|
||||
rdState.contentEntities.forEach(meta => rdState.pageFilterEntities.mutateAndAddToFilters(meta));
|
||||
|
||||
rdState.listEntities.init();
|
||||
|
||||
rdState.pageFilterEntities.trimState();
|
||||
rdState.pageFilterEntities.filterBox.render();
|
||||
|
||||
rdState.pageFilterEntities.filterBox.on(
|
||||
EVNT_VALCHANGE,
|
||||
this._handleFilterChange_entities.bind(this, {rdState}),
|
||||
);
|
||||
|
||||
this._handleFilterChange_entities({rdState});
|
||||
|
||||
$iptSearch.focus();
|
||||
});
|
||||
}
|
||||
|
||||
_handleFilterChange_entities ({rdState}) {
|
||||
const f = rdState.pageFilterEntities.filterBox.getValues();
|
||||
rdState.listEntities.filter(li => rdState.pageFilterEntities.toDisplay(f, rdState.contentEntities[li.ix]));
|
||||
}
|
||||
|
||||
_pRender_getEntityRowMeta ({rdState, prop, ent, ixParent}) {
|
||||
const eleLi = document.createElement("div");
|
||||
eleLi.className = "lst__row ve-flex-col px-0";
|
||||
|
||||
const dispName = this.constructor._getDisplayName({brew: this._brew, ent, prop});
|
||||
const sourceMeta = this.constructor._getSourceMeta({brew: this._brew, ent});
|
||||
const dispProp = this.constructor._getDisplayProp({ent, prop});
|
||||
|
||||
eleLi.innerHTML = `<label class="lst--border lst__row-inner no-select mb-0 ve-flex-v-center">
|
||||
<div class="pl-0 ve-col-1 ve-flex-vh-center"><input type="checkbox" class="no-events"></div>
|
||||
<div class="ve-col-5 bold">${dispName}</div>
|
||||
<div class="ve-col-1 ve-text-center" title="${(sourceMeta.full || "").qq()}" ${this._brewUtil.sourceToStyle(sourceMeta)}>${sourceMeta.abbreviation}</div>
|
||||
<div class="ve-col-5 ve-flex-vh-center pr-0">${dispProp}</div>
|
||||
</label>`;
|
||||
|
||||
const listItem = new ListItem(
|
||||
ixParent, // We identify the item in the list according to its position across all props
|
||||
eleLi,
|
||||
dispName,
|
||||
{
|
||||
source: sourceMeta.abbreviation,
|
||||
category: dispProp,
|
||||
},
|
||||
{
|
||||
cbSel: eleLi.firstElementChild.firstElementChild.firstElementChild,
|
||||
prop,
|
||||
ent,
|
||||
},
|
||||
);
|
||||
|
||||
eleLi.addEventListener("click", evt => rdState.listEntitiesSelectClickHandler.handleSelectClick(listItem, evt));
|
||||
|
||||
return {
|
||||
listItem,
|
||||
};
|
||||
}
|
||||
|
||||
_pRender_tabMetadata ({tabMeta, rdState}) {
|
||||
const infoTuples = Object.entries(this.constructor._PROP_INFOS_META).filter(([k]) => Object.keys(this._brew.body?._meta?.[k] || {}).length);
|
||||
|
||||
if (!infoTuples.length) {
|
||||
$$(tabMeta.$wrpTab)`
|
||||
<h4>Metadata</h4>
|
||||
<p><i>No metadata found.</i></p>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const metasSections = infoTuples
|
||||
.map(([prop, info]) => this._pRender_getMetaRowMeta({prop, info}));
|
||||
|
||||
$$(tabMeta.$wrpTab)`
|
||||
<div class="pt-2"><i>Warning: deleting metadata may invalidate or otherwise corrupt homebrew which depends on it. Use with caution.</i></div>
|
||||
<hr class="hr-3">
|
||||
${metasSections.map(({$wrp}) => $wrp)}
|
||||
`;
|
||||
}
|
||||
|
||||
_pRender_getMetaRowMeta ({prop, info}) {
|
||||
const displayName = info.displayName || prop.toTitleCase();
|
||||
const displayFn = info.displayFn || ((...args) => args.last().toTitleCase());
|
||||
|
||||
const $rows = Object.keys(this._brew.body._meta[prop])
|
||||
.map(k => {
|
||||
const $btnDelete = $(`<button class="btn btn-danger btn-xs" title="Delete"><span class="glyphicon glyphicon-trash"></span></button>`)
|
||||
.click(() => {
|
||||
this._isDirty = true;
|
||||
MiscUtil.deleteObjectPath(this._brew.body._meta, prop, k);
|
||||
$row.remove();
|
||||
|
||||
// If we deleted the last key and the whole prop has therefore been cleaned up, delete the section
|
||||
if (this._brew.body._meta[prop]) return;
|
||||
|
||||
$wrp.remove();
|
||||
});
|
||||
|
||||
const $row = $$`<div class="lst__row ve-flex-col px-0">
|
||||
<div class="split-v-center lst--border lst__row-inner no-select mb-0 ve-flex-v-center">
|
||||
<div class="ve-col-10">${displayFn(this._brew, prop, k)}</div>
|
||||
<div class="ve-col-2 btn-group ve-flex-v-center ve-flex-h-right">
|
||||
${$btnDelete}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return $row;
|
||||
});
|
||||
|
||||
const $wrp = $$`<div class="ve-flex-col mb-4">
|
||||
<div class="bold mb-2">${displayName}:</div>
|
||||
<div class="ve-flex-col list-display-only">${$rows}</div>
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
$wrp,
|
||||
};
|
||||
}
|
||||
|
||||
_pRender_tabSources ({tabMeta, rdState}) {
|
||||
const $cbAll = $(`<input type="checkbox">`);
|
||||
const $wrpRows = $$`<div class="list ve-flex-col w-100 max-h-unset"></div>`;
|
||||
const $iptSearch = $(`<input type="search" class="search manbrew__search form-control w-100 mt-1" placeholder="Search source...">`);
|
||||
const $wrpBtnsSort = $$`<div class="filtertools manbrew__filtertools input-group input-group--bottom ve-flex no-shrink">
|
||||
<label class="btn btn-default btn-xs ve-col-1 pr-0 ve-flex-vh-center">${$cbAll}</label>
|
||||
<button class="ve-col-5 sort btn btn-default btn-xs" data-sort="name">Name</button>
|
||||
<button class="ve-col-2 sort btn btn-default btn-xs" data-sort="abbreviation">Abbreviation</button>
|
||||
<button class="ve-col-4 sort btn btn-default btn-xs" data-sort="json">JSON</button>
|
||||
</div>`;
|
||||
|
||||
$$(tabMeta.$wrpTab)`
|
||||
${$iptSearch}
|
||||
${$wrpBtnsSort}
|
||||
${$wrpRows}`;
|
||||
|
||||
rdState.listSources = new List({
|
||||
$iptSearch,
|
||||
$wrpList: $wrpRows,
|
||||
fnSort: SortUtil.listSort,
|
||||
});
|
||||
|
||||
rdState.listSourcesSelectClickHandler = new ListSelectClickHandler({list: rdState.listSources});
|
||||
rdState.listSourcesSelectClickHandler.bindSelectAllCheckbox($cbAll);
|
||||
SortUtil.initBtnSortHandlers($wrpBtnsSort, rdState.listSources);
|
||||
|
||||
(this._brew.body?._meta?.sources || [])
|
||||
.forEach((source, ix) => {
|
||||
const {listItem} = this._pRender_getSourceRowMeta({rdState, source, ix});
|
||||
rdState.listSources.addItem(listItem);
|
||||
});
|
||||
|
||||
rdState.listSources.init();
|
||||
$iptSearch.focus();
|
||||
}
|
||||
|
||||
_pRender_getSourceRowMeta ({rdState, source, ix}) {
|
||||
const eleLi = document.createElement("div");
|
||||
eleLi.className = "lst__row ve-flex-col px-0";
|
||||
|
||||
const name = source.full || SOURCE_UNKNOWN_FULL;
|
||||
const abv = source.abbreviation || SOURCE_UNKNOWN_ABBREVIATION;
|
||||
|
||||
eleLi.innerHTML = `<label class="lst--border lst__row-inner no-select mb-0 ve-flex-v-center">
|
||||
<div class="pl-0 ve-col-1 ve-flex-vh-center"><input type="checkbox" class="no-events"></div>
|
||||
<div class="ve-col-5 bold">${name}</div>
|
||||
<div class="ve-col-2 ve-text-center">${abv}</div>
|
||||
<div class="ve-col-4 ve-flex-vh-center pr-0">${source.json}</div>
|
||||
</label>`;
|
||||
|
||||
const listItem = new ListItem(
|
||||
ix,
|
||||
eleLi,
|
||||
name,
|
||||
{
|
||||
abbreviation: abv,
|
||||
json: source.json,
|
||||
},
|
||||
{
|
||||
cbSel: eleLi.firstElementChild.firstElementChild.firstElementChild,
|
||||
source,
|
||||
},
|
||||
);
|
||||
|
||||
eleLi.addEventListener("click", evt => rdState.listSourcesSelectClickHandler.handleSelectClick(listItem, evt));
|
||||
|
||||
return {
|
||||
listItem,
|
||||
};
|
||||
}
|
||||
|
||||
static _NAME_UNKNOWN = "(Unknown)";
|
||||
|
||||
static _getDisplayName ({brew, ent, prop}) {
|
||||
switch (prop) {
|
||||
case "itemProperty": {
|
||||
if (ent.name) return ent.name || this._NAME_UNKNOWN;
|
||||
if (ent.entries) {
|
||||
const name = Renderer.findName(ent.entries);
|
||||
if (name) return name;
|
||||
}
|
||||
if (ent.entriesTemplate) {
|
||||
const name = Renderer.findName(ent.entriesTemplate);
|
||||
if (name) return name;
|
||||
}
|
||||
return ent.abbreviation || this._NAME_UNKNOWN;
|
||||
}
|
||||
|
||||
case "adventureData":
|
||||
case "bookData": {
|
||||
const propContents = prop === "adventureData" ? "adventure" : "book";
|
||||
|
||||
if (!brew[propContents]) return ent.id || this._NAME_UNKNOWN;
|
||||
|
||||
return brew[propContents].find(it => it.id === ent.id)?.name || ent.id || this._NAME_UNKNOWN;
|
||||
}
|
||||
|
||||
default: return ent.name || this._NAME_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static _getSourceMeta ({brew, ent}) {
|
||||
const entSource = SourceUtil.getEntitySource(ent);
|
||||
if (!entSource) return {abbreviation: SOURCE_UNKNOWN_ABBREVIATION, full: SOURCE_UNKNOWN_FULL};
|
||||
const source = (brew.body?._meta?.sources || []).find(src => src.json === entSource);
|
||||
if (!source) return {abbreviation: SOURCE_UNKNOWN_ABBREVIATION, full: SOURCE_UNKNOWN_FULL};
|
||||
return source;
|
||||
}
|
||||
|
||||
static _getDisplayProp ({ent, prop}) {
|
||||
const out = [Parser.getPropDisplayName(prop)];
|
||||
|
||||
switch (prop) {
|
||||
case "subclass": out.push(` (${ent.className})`); break;
|
||||
case "subrace": out.push(` (${ent.raceName})`); break;
|
||||
case "psionic": out.push(` (${Parser.psiTypeToMeta(ent.type).short})`); break;
|
||||
}
|
||||
|
||||
return out.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
/** These are props found in "_meta" sections of files */
|
||||
static _PROP_INFOS_META = {
|
||||
"spellDistanceUnits": {
|
||||
displayName: "Spell Distance Units",
|
||||
},
|
||||
"spellSchools": {
|
||||
displayName: "Spell Schools",
|
||||
displayFn: (brew, propMeta, k) => brew.body._meta[propMeta][k].full || k,
|
||||
},
|
||||
"currencyConversions": {
|
||||
displayName: "Currency Conversion Tables",
|
||||
displayFn: (brew, propMeta, k) => `${k}: ${brew.body._meta[propMeta][k].map(it => `${it.coin}=${it.mult}`).join(", ")}`,
|
||||
},
|
||||
"skills": {
|
||||
displayName: "Skills",
|
||||
},
|
||||
"senses": {
|
||||
displayName: "Senses",
|
||||
},
|
||||
"optionalFeatureTypes": {
|
||||
displayName: "Optional Feature Types",
|
||||
displayFn: (brew, propMeta, k) => brew.body._meta[propMeta][k] || k,
|
||||
},
|
||||
"charOption": {
|
||||
displayName: "Character Creation Option Types",
|
||||
displayFn: (brew, propMeta, k) => brew.body._meta[propMeta][k] || k,
|
||||
},
|
||||
"psionicTypes": {
|
||||
displayName: "Psionic Types",
|
||||
displayFn: (brew, propMeta, k) => brew.body._meta[propMeta][k].full || k,
|
||||
},
|
||||
};
|
||||
}
|
||||
981
js/utils-brew/utils-brew-ui-manage.js
Normal file
981
js/utils-brew/utils-brew-ui-manage.js
Normal file
@@ -0,0 +1,981 @@
|
||||
import {SOURCE_UNKNOWN_ABBREVIATION, SOURCE_UNKNOWN_FULL} from "./utils-brew-constants.js";
|
||||
import {BrewDoc} from "./utils-brew-models.js";
|
||||
import {GetBrewUi} from "./utils-brew-ui-get.js";
|
||||
import {ManageEditableBrewContentsUi} from "./utils-brew-ui-manage-editable-contents.js";
|
||||
import {ManageExternalUtils} from "../manageexternal/manageexternal-utils.js";
|
||||
|
||||
export class ManageBrewUi {
|
||||
static _RenderState = class {
|
||||
constructor () {
|
||||
this.$stgBrewList = null;
|
||||
this.list = null;
|
||||
this.listSelectClickHandler = null;
|
||||
this.brews = [];
|
||||
this.menuListMass = null;
|
||||
this.rowMetas = [];
|
||||
}
|
||||
};
|
||||
|
||||
constructor ({brewUtil, isModal = false} = {}) {
|
||||
this._brewUtil = brewUtil;
|
||||
this._isModal = isModal;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static _CONTEXT_MENU_BTNGROUP_MANAGER = null;
|
||||
|
||||
static bindBtngroupManager (btngroup) {
|
||||
btngroup
|
||||
.first(`[name="manage-content"]`)
|
||||
.onn("click", evt => this._pOnClickBtnManageContent({evt}));
|
||||
|
||||
btngroup
|
||||
.first(`[name="manage-prerelease"]`)
|
||||
.onn("click", evt => this._onClickBtnManagePrereleaseBrew({brewUtil: PrereleaseUtil, isGoToPage: evt.shiftKey}));
|
||||
|
||||
btngroup
|
||||
.first(`[name="manage-brew"]`)
|
||||
.onn("click", evt => this._onClickBtnManagePrereleaseBrew({brewUtil: BrewUtil2, isGoToPage: evt.shiftKey}));
|
||||
}
|
||||
|
||||
static bindBtnOpen ($btn, {brewUtil = null} = {}) {
|
||||
brewUtil = brewUtil || BrewUtil2;
|
||||
|
||||
$btn.click(evt => this._onClickBtnManagePrereleaseBrew({brewUtil, isGoToPage: evt.shiftKey}));
|
||||
}
|
||||
|
||||
static _pOnClickBtnManageContent ({evt}) {
|
||||
this._CONTEXT_MENU_BTNGROUP_MANAGER ||= ContextUtil.getMenu([
|
||||
new ContextUtil.Action(
|
||||
"Manage Prerelease Content",
|
||||
async evt => {
|
||||
this._onClickBtnManagePrereleaseBrew({brewUtil: PrereleaseUtil, isGoToPage: evt.shiftKey});
|
||||
},
|
||||
),
|
||||
new ContextUtil.Action(
|
||||
"Manage Homebrew",
|
||||
async evt => {
|
||||
this._onClickBtnManagePrereleaseBrew({brewUtil: BrewUtil2, isGoToPage: evt.shiftKey});
|
||||
},
|
||||
),
|
||||
null,
|
||||
new ContextUtil.Action(
|
||||
"Load All Partnered Content",
|
||||
async evt => {
|
||||
await this.pOnClickBtnLoadAllPartnered();
|
||||
},
|
||||
),
|
||||
null,
|
||||
new ContextUtil.Action(
|
||||
"Delete All Loaded Content",
|
||||
async evt => {
|
||||
await this._pOnClickBtnDeleteAllLoadedContent();
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return ContextUtil.pOpenMenu(evt, this._CONTEXT_MENU_BTNGROUP_MANAGER);
|
||||
}
|
||||
|
||||
static _onClickBtnManagePrereleaseBrew ({brewUtil, isGoToPage}) {
|
||||
if (isGoToPage) return window.location = brewUtil.PAGE_MANAGE;
|
||||
return this.pDoManageBrew({brewUtil});
|
||||
}
|
||||
|
||||
static async pOnClickBtnLoadAllPartnered () {
|
||||
const brewDocs = [];
|
||||
try {
|
||||
const [brewDocsPrerelease, brewDocsHomebrew] = await Promise.all([
|
||||
PrereleaseUtil.pAddBrewsPartnered({isSilent: true}),
|
||||
BrewUtil2.pAddBrewsPartnered({isSilent: true}),
|
||||
]);
|
||||
brewDocs.push(
|
||||
...brewDocsPrerelease,
|
||||
...brewDocsHomebrew,
|
||||
);
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({type: "danger", content: `Failed to load partnered content! ${VeCt.STR_SEE_CONSOLE}`});
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (brewDocs.length) JqueryUtil.doToast(`Loaded partnered content!`);
|
||||
|
||||
if (PrereleaseUtil.isReloadRequired()) PrereleaseUtil.doLocationReload();
|
||||
if (BrewUtil2.isReloadRequired()) BrewUtil2.doLocationReload();
|
||||
}
|
||||
|
||||
static async _pOnClickBtnDeleteAllLoadedContent () {
|
||||
if (
|
||||
!await InputUiUtil.pGetUserBoolean({
|
||||
title: `Delete All Loaded ${PrereleaseUtil.DISPLAY_NAME.toTitleCase()} and ${BrewUtil2.DISPLAY_NAME.toTitleCase()}`,
|
||||
htmlDescription: `<div>
|
||||
<div>Are you sure?</div>
|
||||
<div class="ve-muted"><i>Note that this will <b>not</b> delete your Editable ${PrereleaseUtil.DISPLAY_NAME.toTitleCase()} and Editable ${BrewUtil2.DISPLAY_NAME.toTitleCase()}.</i></div>
|
||||
</div>`,
|
||||
textYes: "Yes",
|
||||
textNo: "Cancel",
|
||||
})
|
||||
) return;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
PrereleaseUtil.pDeleteUneditableBrews(),
|
||||
BrewUtil2.pDeleteUneditableBrews(),
|
||||
]);
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({type: "danger", content: `Failed to load partnered content! ${VeCt.STR_SEE_CONSOLE}`});
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (PrereleaseUtil.isReloadRequired()) PrereleaseUtil.doLocationReload();
|
||||
if (BrewUtil2.isReloadRequired()) BrewUtil2.doLocationReload();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static async pOnClickBtnExportListAsUrl ({ele}) {
|
||||
const url = await ManageExternalUtils.pGetUrl();
|
||||
await MiscUtil.pCopyTextToClipboard(url);
|
||||
JqueryUtil.showCopiedEffect(ele);
|
||||
|
||||
if (
|
||||
!await PrereleaseUtil.pHasEditableSourceJson()
|
||||
&& !await BrewUtil2.pHasEditableSourceJson()
|
||||
) return;
|
||||
|
||||
JqueryUtil.doToast({type: "warning", content: `Note: you have Editable ${PrereleaseUtil.DISPLAY_NAME} or ${BrewUtil2.DISPLAY_NAME}. This cannot be exported as part of a URL, and so was not included.`});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static async pDoManageBrew ({brewUtil = null} = {}) {
|
||||
brewUtil = brewUtil || BrewUtil2;
|
||||
|
||||
const ui = new this({isModal: true, brewUtil});
|
||||
const rdState = new this._RenderState();
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
isHeight100: true,
|
||||
isWidth100: true,
|
||||
title: `Manage ${brewUtil.DISPLAY_NAME.toTitleCase()}`,
|
||||
isUncappedHeight: true,
|
||||
$titleSplit: $$`<div class="ve-flex-v-center btn-group">
|
||||
${ui._$getBtnPullAll(rdState)}
|
||||
${ui._$getBtnDeleteAll(rdState)}
|
||||
</div>`,
|
||||
isHeaderBorder: true,
|
||||
cbClose: () => {
|
||||
if (!brewUtil.isReloadRequired()) return;
|
||||
brewUtil.doLocationReload();
|
||||
},
|
||||
});
|
||||
await ui.pRender($modalInner, {rdState});
|
||||
}
|
||||
|
||||
_$getBtnDeleteAll (rdState) {
|
||||
const brewUtilOther = this._brewUtil === PrereleaseUtil ? BrewUtil2 : PrereleaseUtil;
|
||||
|
||||
return $(`<button class="btn btn-danger" title="SHIFT to also delete all ${brewUtilOther.DISPLAY_NAME.toTitleCase()}">Delete All</button>`)
|
||||
.addClass(this._isModal ? "btn-xs" : "btn-sm")
|
||||
.click(async evt => {
|
||||
if (!evt.shiftKey) {
|
||||
if (!await InputUiUtil.pGetUserBoolean({title: `Delete All ${this._brewUtil.DISPLAY_NAME.toTitleCase()}`, htmlDescription: "Are you sure?", textYes: "Yes", textNo: "Cancel"})) return;
|
||||
|
||||
await this._pDoDeleteAll(rdState);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!await InputUiUtil.pGetUserBoolean({
|
||||
title: `Delete All ${this._brewUtil.DISPLAY_NAME.toTitleCase()} and ${brewUtilOther.DISPLAY_NAME.toTitleCase()}`,
|
||||
htmlDescription: "Are you sure?",
|
||||
textYes: "Yes",
|
||||
textNo: "Cancel",
|
||||
})
|
||||
) return;
|
||||
|
||||
await brewUtilOther.pSetBrew([]);
|
||||
await this._pDoDeleteAll(rdState);
|
||||
});
|
||||
}
|
||||
|
||||
_$getBtnPullAll (rdState) {
|
||||
const $btn = $(`<button class="btn btn-default">Update All</button>`)
|
||||
.addClass(this._isModal ? "btn-xs w-70p" : "btn-sm w-80p")
|
||||
.click(async () => {
|
||||
const cachedHtml = $btn.html();
|
||||
|
||||
try {
|
||||
$btn.text(`Updating...`).prop("disabled", true);
|
||||
await this._pDoPullAll({rdState});
|
||||
} catch (e) {
|
||||
$btn.text(`Failed!`);
|
||||
setTimeout(() => $btn.html(cachedHtml).prop("disabled", false), VeCt.DUR_INLINE_NOTIFY);
|
||||
throw e;
|
||||
}
|
||||
|
||||
$btn.text(`Done!`);
|
||||
setTimeout(() => $btn.html(cachedHtml).prop("disabled", false), VeCt.DUR_INLINE_NOTIFY);
|
||||
});
|
||||
return $btn;
|
||||
}
|
||||
|
||||
async _pDoDeleteAll (rdState) {
|
||||
await this._brewUtil.pSetBrew([]);
|
||||
|
||||
rdState.list.removeAllItems();
|
||||
rdState.list.update();
|
||||
}
|
||||
|
||||
async _pDoPullAll ({rdState, brews = null}) {
|
||||
if (brews && !brews.length) return;
|
||||
|
||||
let cntPulls;
|
||||
try {
|
||||
cntPulls = await this._brewUtil.pPullAllBrews({brews});
|
||||
} catch (e) {
|
||||
JqueryUtil.doToast({content: `Update failed! ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
|
||||
throw e;
|
||||
}
|
||||
if (!cntPulls) return JqueryUtil.doToast(`Update complete! No ${this._brewUtil.DISPLAY_NAME} was updated.`);
|
||||
|
||||
await this._pRender_pBrewList(rdState);
|
||||
JqueryUtil.doToast(`Update complete! ${cntPulls} ${cntPulls === 1 ? `${this._brewUtil.DISPLAY_NAME} was` : `${this._brewUtil.DISPLAY_NAME_PLURAL} were`} updated.`);
|
||||
}
|
||||
|
||||
async pRender ($wrp, {rdState = null} = {}) {
|
||||
rdState = rdState || new this.constructor._RenderState();
|
||||
|
||||
rdState.$stgBrewList = $(`<div class="manbrew__current_brew ve-flex-col h-100 mt-1 min-h-0"></div>`);
|
||||
|
||||
await this._pRender_pBrewList(rdState);
|
||||
|
||||
const btnLoadPartnered = ee`<button class="btn btn-default btn-sm">Load All Partnered</button>`
|
||||
.onn("click", () => this._pHandleClick_btnLoadPartnered(rdState));
|
||||
|
||||
const $btnLoadFromFile = $(`<button class="btn btn-default btn-sm">Load from File</button>`)
|
||||
.click(() => this._pHandleClick_btnLoadFromFile(rdState));
|
||||
|
||||
const $btnLoadFromUrl = $(`<button class="btn btn-default btn-sm">Load from URL</button>`)
|
||||
.click(() => this._pHandleClick_btnLoadFromUrl(rdState));
|
||||
|
||||
const $btnGet = $(`<button class="btn ${this._brewUtil.STYLE_BTN} btn-sm">Get ${this._brewUtil.DISPLAY_NAME.toTitleCase()}</button>`)
|
||||
.click(() => this._pHandleClick_btnGetBrew(rdState));
|
||||
|
||||
const $btnCustomUrl = $(`<button class="btn ${this._brewUtil.STYLE_BTN} btn-sm px-2" title="Set Custom Repository URL"><span class="glyphicon glyphicon-cog"></span></button>`)
|
||||
.click(() => this._pHandleClick_btnSetCustomRepo());
|
||||
|
||||
const $btnPullAll = this._isModal ? null : this._$getBtnPullAll(rdState);
|
||||
const $btnDeleteAll = this._isModal ? null : this._$getBtnDeleteAll(rdState);
|
||||
|
||||
const $btnSaveToUrl = $(`<button class="btn btn-default btn-sm" title="Note that this does not include "Editable" or "Local" content.">Export List as URL</button>`)
|
||||
.click(async evt => {
|
||||
await this.constructor.pOnClickBtnExportListAsUrl({ele: evt.originalEvent.currentTarget});
|
||||
});
|
||||
|
||||
const $wrpBtns = $$`<div class="ve-flex-v-center no-shrink mobile__ve-flex-col">
|
||||
<div class="ve-flex-v-center mobile__mb-2">
|
||||
<div class="ve-flex-v-center btn-group mr-2">
|
||||
${$btnGet}
|
||||
${$btnCustomUrl}
|
||||
</div>
|
||||
<div class="ve-flex-v-center btn-group mr-2">
|
||||
${btnLoadPartnered}
|
||||
</div>
|
||||
<div class="ve-flex-v-center btn-group mr-2">
|
||||
${$btnLoadFromFile}
|
||||
${$btnLoadFromUrl}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ve-flex-v-center">
|
||||
<a href="${this._brewUtil.URL_REPO_DEFAULT}" class="ve-flex-v-center" target="_blank" rel="noopener noreferrer"><button class="btn btn-default btn-sm mr-2">Browse Source Repository</button></a>
|
||||
|
||||
<div class="ve-flex-v-center btn-group mr-2">
|
||||
${$btnSaveToUrl}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex-v-center btn-group">
|
||||
${$btnPullAll}
|
||||
${$btnDeleteAll}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (this._isModal) {
|
||||
$$($wrp)`
|
||||
${rdState.$stgBrewList}
|
||||
${$wrpBtns.addClass("mb-2")}`;
|
||||
} else {
|
||||
$$($wrp)`
|
||||
${$wrpBtns.addClass("mb-3")}
|
||||
${rdState.$stgBrewList}`;
|
||||
}
|
||||
}
|
||||
|
||||
async _pHandleClick_btnLoadPartnered (rdState) {
|
||||
await this._brewUtil.pAddBrewsPartnered();
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
async _pHandleClick_btnLoadFromFile (rdState) {
|
||||
const {files, errors} = await InputUiUtil.pGetUserUploadJson({isMultiple: true});
|
||||
|
||||
DataUtil.doHandleFileLoadErrorsGeneric(errors);
|
||||
|
||||
await this._brewUtil.pAddBrewsFromFiles(files);
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
async _pHandleClick_btnLoadFromUrl (rdState) {
|
||||
const enteredUrl = await InputUiUtil.pGetUserString({title: `${this._brewUtil.DISPLAY_NAME.toTitleCase()} URL`});
|
||||
if (!enteredUrl || !enteredUrl.trim()) return;
|
||||
|
||||
const parsedUrl = this.constructor._getParsedCustomUrl(enteredUrl);
|
||||
if (!parsedUrl) {
|
||||
return JqueryUtil.doToast({
|
||||
content: `The URL was not valid!`,
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
|
||||
await this._brewUtil.pAddBrewFromUrl(parsedUrl.href);
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
static _getParsedCustomUrl (enteredUrl) {
|
||||
try {
|
||||
return new URL(enteredUrl);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async _pHandleClick_btnGetBrew (rdState) {
|
||||
await GetBrewUi.pDoGetBrew({brewUtil: this._brewUtil, isModal: this._isModal});
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
async _pHandleClick_btnSetCustomRepo () {
|
||||
const customBrewUtl = await this._brewUtil.pGetCustomUrl();
|
||||
|
||||
const nxtUrl = await InputUiUtil.pGetUserString({
|
||||
title: `${this._brewUtil.DISPLAY_NAME.toTitleCase()} Repository URL`,
|
||||
$elePre: $(`<div>
|
||||
<p>Leave blank to use the <a href="${this._brewUtil.URL_REPO_DEFAULT}" rel="noopener noreferrer" target="_blank">default ${this._brewUtil.DISPLAY_NAME} repo</a>.</p>
|
||||
<div>Note that for GitHub URLs, the <code>raw.</code> URL must be used. For example, <code>${this._brewUtil.URL_REPO_ROOT_DEFAULT.replace(/TheGiddyLimit/g, "YourUsernameHere")}</code></div>
|
||||
<hr class="hr-3">
|
||||
</div>`),
|
||||
default: customBrewUtl,
|
||||
});
|
||||
if (nxtUrl == null) return;
|
||||
|
||||
await this._brewUtil.pSetCustomUrl(nxtUrl);
|
||||
}
|
||||
|
||||
async _pRender_pBrewList (rdState) {
|
||||
rdState.$stgBrewList.empty();
|
||||
rdState.rowMetas.splice(0, rdState.rowMetas.length)
|
||||
.forEach(({menu}) => ContextUtil.deleteMenu(menu));
|
||||
|
||||
const $btnMass = $(`<button class="btn btn-default">Mass...</button>`)
|
||||
.click(evt => this._pHandleClick_btnListMass({evt, rdState}));
|
||||
const $iptSearch = $(`<input type="search" class="search manbrew__search form-control" placeholder="Search ${this._brewUtil.DISPLAY_NAME}...">`);
|
||||
const $cbAll = $(`<input type="checkbox">`);
|
||||
const $wrpList = $(`<div class="list-display-only max-h-unset smooth-scroll ve-overflow-y-auto h-100 min-h-0 brew-list brew-list--target manbrew__list relative ve-flex-col w-100 mb-3"></div>`);
|
||||
|
||||
rdState.list = new List({
|
||||
$iptSearch,
|
||||
$wrpList,
|
||||
isUseJquery: true,
|
||||
isFuzzy: true,
|
||||
sortByInitial: rdState.list ? rdState.list.sortBy : undefined,
|
||||
sortDirInitial: rdState.list ? rdState.list.sortDir : undefined,
|
||||
});
|
||||
|
||||
const $wrpBtnsSort = $$`<div class="filtertools manbrew__filtertools btn-group input-group input-group--bottom ve-flex no-shrink">
|
||||
<label class="ve-col-0-5 pr-0 btn btn-default btn-xs ve-flex-vh-center">${$cbAll}</label>
|
||||
<button class="ve-col-1 btn btn-default btn-xs" disabled>Type</button>
|
||||
<button class="ve-col-3 btn btn-default btn-xs" data-sort="source">Source</button>
|
||||
<button class="ve-col-3 btn btn-default btn-xs" data-sort="authors">Authors</button>
|
||||
<button class="ve-col-3 btn btn-default btn-xs" disabled>Origin</button>
|
||||
<button class="ve-col-1-5 btn btn-default btn-xs ve-grow" disabled> </button>
|
||||
</div>`;
|
||||
|
||||
$$(rdState.$stgBrewList)`
|
||||
<div class="ve-flex-col h-100">
|
||||
<div class="input-group ve-flex-vh-center">
|
||||
${$btnMass}
|
||||
${$iptSearch}
|
||||
</div>
|
||||
${$wrpBtnsSort}
|
||||
<div class="ve-flex w-100 h-100 min-h-0 relative">${$wrpList}</div>
|
||||
</div>`;
|
||||
|
||||
rdState.listSelectClickHandler = new ListSelectClickHandler({list: rdState.list});
|
||||
rdState.listSelectClickHandler.bindSelectAllCheckbox($cbAll);
|
||||
SortUtil.initBtnSortHandlers($wrpBtnsSort, rdState.list);
|
||||
|
||||
rdState.brews = (await this._brewUtil.pGetBrew()).map(brew => this._pRender_getProcBrew(brew));
|
||||
|
||||
rdState.brews.forEach((brew, ix) => {
|
||||
const meta = this._pRender_getLoadedRowMeta(rdState, brew, ix);
|
||||
rdState.rowMetas.push(meta);
|
||||
rdState.list.addItem(meta.listItem);
|
||||
});
|
||||
|
||||
rdState.list.init();
|
||||
$iptSearch.focus();
|
||||
}
|
||||
|
||||
get _LBL_LIST_UPDATE () { return "Update"; }
|
||||
get _LBL_LIST_MANAGE_CONTENTS () { return "Manage Contents"; }
|
||||
get _LBL_LIST_EXPORT () { return "Export"; }
|
||||
get _LBL_LIST_VIEW_JSON () { return "View JSON"; }
|
||||
get _LBL_LIST_DELETE () { return "Delete"; }
|
||||
get _LBL_LIST_MOVE_TO_EDITABLE () { return `Move to Editable ${this._brewUtil.DISPLAY_NAME.toTitleCase()} Document`; }
|
||||
|
||||
_initListMassMenu ({rdState}) {
|
||||
if (rdState.menuListMass) return;
|
||||
|
||||
const getSelBrews = ({fnFilter = null} = {}) => {
|
||||
const brews = rdState.list.items
|
||||
.filter(li => li.data.cbSel.checked)
|
||||
.map(li => rdState.brews[li.ix])
|
||||
.filter(brew => fnFilter ? fnFilter(brew) : true);
|
||||
|
||||
if (!brews.length) JqueryUtil.doToast({content: `Please select some suitable ${this._brewUtil.DISPLAY_NAME_PLURAL} first!`, type: "warning"});
|
||||
|
||||
return brews;
|
||||
};
|
||||
|
||||
rdState.menuListMass = ContextUtil.getMenu([
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_UPDATE,
|
||||
async () => this._pDoPullAll({
|
||||
rdState,
|
||||
brews: getSelBrews(),
|
||||
}),
|
||||
),
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_EXPORT,
|
||||
async () => {
|
||||
for (const brew of getSelBrews()) await this._pRender_pDoDownloadBrew({brew});
|
||||
},
|
||||
),
|
||||
this._brewUtil.IS_EDITABLE
|
||||
? new ContextUtil.Action(
|
||||
this._LBL_LIST_MOVE_TO_EDITABLE,
|
||||
async () => this._pRender_pDoMoveToEditable({
|
||||
rdState,
|
||||
brews: getSelBrews({
|
||||
fnFilter: brew => this._isBrewOperationPermitted_moveToEditable(brew),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_DELETE,
|
||||
async () => this._pRender_pDoDelete({
|
||||
rdState,
|
||||
brews: getSelBrews({
|
||||
fnFilter: brew => this._isBrewOperationPermitted_delete(brew),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
].filter(Boolean));
|
||||
}
|
||||
|
||||
_isBrewOperationPermitted_update (brew) { return this._brewUtil.isPullable(brew); }
|
||||
_isBrewOperationPermitted_moveToEditable (brew) { return BrewDoc.isOperationPermitted_moveToEditable({brew}); }
|
||||
_isBrewOperationPermitted_delete (brew) { return !brew.head.isLocal; }
|
||||
|
||||
async _pHandleClick_btnListMass ({evt, rdState}) {
|
||||
this._initListMassMenu({rdState});
|
||||
await ContextUtil.pOpenMenu(evt, rdState.menuListMass);
|
||||
}
|
||||
|
||||
static _getBrewName (brew) {
|
||||
const sources = brew.body._meta?.sources || [];
|
||||
|
||||
return sources
|
||||
.map(brewSource => brewSource.full || SOURCE_UNKNOWN_FULL)
|
||||
.sort(SortUtil.ascSortLower)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
_pRender_getLoadedRowMeta (rdState, brew, ix) {
|
||||
const sources = brew.body._meta?.sources || [];
|
||||
|
||||
const rowsSubMetas = sources
|
||||
.map(brewSource => {
|
||||
const hasConverters = !!brewSource.convertedBy?.length;
|
||||
const btnConvertedBy = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-xxs btn-default ${!hasConverters ? "disabled" : ""}`,
|
||||
title: hasConverters ? `Converted by: ${brewSource.convertedBy.join(", ").qq()}` : "(No conversion credit given)",
|
||||
children: [
|
||||
e_({tag: "span", clazz: "mobile__hidden", text: "View Converters"}),
|
||||
e_({tag: "span", clazz: "mobile__visible", text: "Convs.", title: "View Converters"}),
|
||||
],
|
||||
click: () => {
|
||||
if (!hasConverters) return;
|
||||
const {$modalInner} = UiUtil.getShowModal({
|
||||
title: `Converted By:${brewSource.convertedBy.length === 1 ? ` ${brewSource.convertedBy.join("")}` : ""}`,
|
||||
isMinHeight0: true,
|
||||
});
|
||||
|
||||
if (brewSource.convertedBy.length === 1) return;
|
||||
$modalInner.append(`<ul>${brewSource.convertedBy.map(it => `<li>${it.qq()}</li>`).join("")}</ul>`);
|
||||
},
|
||||
});
|
||||
|
||||
const authorsFull = [(brewSource.authors || [])].flat(2).join(", ");
|
||||
|
||||
const lnkUrl = brewSource.url
|
||||
? e_({
|
||||
tag: "a",
|
||||
clazz: "ve-col-2 ve-text-center",
|
||||
href: brewSource.url,
|
||||
attrs: {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
},
|
||||
text: "View Source",
|
||||
})
|
||||
: e_({
|
||||
tag: "span",
|
||||
clazz: "ve-col-2 ve-text-center",
|
||||
});
|
||||
|
||||
const eleRow = e_({
|
||||
tag: "div",
|
||||
clazz: `w-100 ve-flex-v-center`,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: `ve-col-4 manbrew__source px-1`,
|
||||
text: brewSource.full,
|
||||
}),
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: `ve-col-4 px-1`,
|
||||
text: authorsFull,
|
||||
}),
|
||||
lnkUrl,
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `ve-flex-vh-center ve-grow`,
|
||||
children: [
|
||||
btnConvertedBy,
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
eleRow,
|
||||
authorsFull,
|
||||
name: brewSource.full || SOURCE_UNKNOWN_FULL,
|
||||
abbreviation: brewSource.abbreviation || SOURCE_UNKNOWN_ABBREVIATION,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => SortUtil.ascSortLower(a.name, b.name));
|
||||
|
||||
const brewName = this.constructor._getBrewName(brew);
|
||||
|
||||
// region These are mutually exclusive
|
||||
const btnPull = this._pRender_getBtnPull({rdState, brew});
|
||||
const btnEdit = this._pRender_getBtnEdit({rdState, brew});
|
||||
const btnPullEditPlaceholder = (btnPull || btnEdit) ? null : this.constructor._pRender_getBtnPlaceholder();
|
||||
// endregion
|
||||
|
||||
const btnDownload = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs mobile__hidden w-24p`,
|
||||
title: this._LBL_LIST_EXPORT,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "glyphicon glyphicon-download manbrew-row__icn-btn",
|
||||
}),
|
||||
],
|
||||
click: () => this._pRender_pDoDownloadBrew({brew, brewName}),
|
||||
});
|
||||
|
||||
const btnViewJson = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs mobile-lg__hidden w-24p`,
|
||||
title: `${this._LBL_LIST_VIEW_JSON}: ${this.constructor._getBrewJsonTitle({brew, brewName})}`,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "ve-bolder code relative manbrew-row__icn-btn--text",
|
||||
text: "{}",
|
||||
}),
|
||||
],
|
||||
click: evt => this._pRender_doViewBrew({evt, brew, brewName}),
|
||||
});
|
||||
|
||||
const btnOpenMenu = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs w-24p`,
|
||||
title: "Menu",
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "glyphicon glyphicon-option-vertical manbrew-row__icn-btn",
|
||||
}),
|
||||
],
|
||||
click: evt => this._pRender_pDoOpenBrewMenu({evt, rdState, brew, brewName, rowMeta}),
|
||||
});
|
||||
|
||||
const btnDelete = this._isBrewOperationPermitted_delete(brew) ? e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-danger btn-xs mobile__hidden w-24p`,
|
||||
title: this._LBL_LIST_DELETE,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "glyphicon glyphicon-trash manbrew-row__icn-btn",
|
||||
}),
|
||||
],
|
||||
click: () => this._pRender_pDoDelete({rdState, brews: [brew]}),
|
||||
}) : this.constructor._pRender_getBtnPlaceholder();
|
||||
|
||||
// Weave in HRs
|
||||
const elesSub = rowsSubMetas.map(it => it.eleRow);
|
||||
for (let i = rowsSubMetas.length - 1; i > 0; --i) elesSub.splice(i, 0, e_({tag: "hr", clazz: `hr-1 hr--dotted`}));
|
||||
|
||||
const cbSel = e_({
|
||||
tag: "input",
|
||||
clazz: "no-events",
|
||||
type: "checkbox",
|
||||
});
|
||||
|
||||
const ptCategory = brew.head.isLocal
|
||||
? {short: `Local`, title: `Local Document`}
|
||||
: brew.head.isEditable
|
||||
? {short: `Editable`, title: `Editable Document`}
|
||||
: {short: `Standard`, title: `Standard Document`};
|
||||
|
||||
const eleLi = e_({
|
||||
tag: "div",
|
||||
clazz: `manbrew__row ve-flex-v-center lst__row lst--border lst__row-inner no-shrink py-1 no-select`,
|
||||
children: [
|
||||
e_({
|
||||
tag: "label",
|
||||
clazz: `ve-col-0-5 ve-flex-vh-center ve-self-flex-stretch`,
|
||||
children: [cbSel],
|
||||
}),
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `ve-col-1 ve-text-center italic mobile__text-clip-ellipsis`,
|
||||
title: ptCategory.title,
|
||||
text: ptCategory.short,
|
||||
}),
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `ve-col-9 ve-flex-col`,
|
||||
children: elesSub,
|
||||
}),
|
||||
e_({
|
||||
tag: "div",
|
||||
clazz: `ve-col-1-5 btn-group ve-flex-vh-center`,
|
||||
children: [
|
||||
btnPull,
|
||||
btnEdit,
|
||||
btnPullEditPlaceholder,
|
||||
btnDownload,
|
||||
btnViewJson,
|
||||
btnOpenMenu,
|
||||
btnDelete,
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const listItem = new ListItem(
|
||||
ix,
|
||||
eleLi,
|
||||
brewName,
|
||||
{
|
||||
authors: rowsSubMetas.map(it => it.authorsFull).join(", "),
|
||||
abbreviation: rowsSubMetas.map(it => it.abbreviation).join(", "),
|
||||
},
|
||||
{
|
||||
cbSel,
|
||||
},
|
||||
);
|
||||
|
||||
eleLi.addEventListener("click", evt => rdState.listSelectClickHandler.handleSelectClick(listItem, evt, {isPassThroughEvents: true}));
|
||||
|
||||
const rowMeta = {
|
||||
listItem,
|
||||
menu: null,
|
||||
};
|
||||
return rowMeta;
|
||||
}
|
||||
|
||||
static _pRender_getBtnPlaceholder () {
|
||||
return e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs mobile__hidden w-24p`,
|
||||
html: " ",
|
||||
})
|
||||
.attr("disabled", true);
|
||||
}
|
||||
|
||||
_pRender_getBtnPull ({rdState, brew}) {
|
||||
if (!this._isBrewOperationPermitted_update(brew)) return null;
|
||||
|
||||
const btnPull = e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs mobile__hidden w-24p`,
|
||||
title: this._LBL_LIST_UPDATE,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "glyphicon glyphicon-refresh manbrew-row__icn-btn",
|
||||
}),
|
||||
],
|
||||
click: () => this._pRender_pDoPullBrew({rdState, brew}),
|
||||
});
|
||||
if (!this._brewUtil.isPullable(brew)) btnPull.attr("disabled", true).attr("title", `(Update disabled\u2014no URL available)`);
|
||||
return btnPull;
|
||||
}
|
||||
|
||||
_pRender_getBtnEdit ({rdState, brew}) {
|
||||
if (!brew.head.isEditable) return null;
|
||||
|
||||
return e_({
|
||||
tag: "button",
|
||||
clazz: `btn btn-default btn-xs mobile__hidden w-24p`,
|
||||
title: this._LBL_LIST_MANAGE_CONTENTS,
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: "glyphicon glyphicon-pencil manbrew-row__icn-btn",
|
||||
}),
|
||||
],
|
||||
click: () => this._pRender_pDoEditBrew({rdState, brew}),
|
||||
});
|
||||
}
|
||||
|
||||
async _pRender_pDoPullBrew ({rdState, brew}) {
|
||||
const isPull = await this._brewUtil.pPullBrew(brew);
|
||||
|
||||
JqueryUtil.doToast(
|
||||
isPull
|
||||
? `${this._brewUtil.DISPLAY_NAME.uppercaseFirst()} updated!`
|
||||
: `${this._brewUtil.DISPLAY_NAME.uppercaseFirst()} is already up-to-date.`,
|
||||
);
|
||||
|
||||
if (!isPull) return;
|
||||
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
async _pRender_pDoEditBrew ({rdState, brew}) {
|
||||
const {isDirty, brew: nxtBrew} = await ManageEditableBrewContentsUi.pDoOpen({brewUtil: this._brewUtil, brew, isModal: this._isModal});
|
||||
if (!isDirty) return;
|
||||
|
||||
await this._brewUtil.pUpdateBrew(nxtBrew);
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
async _pRender_pDoDownloadBrew ({brew, brewName = null}) {
|
||||
const filename = (brew.head.filename || "").split(".").slice(0, -1).join(".");
|
||||
|
||||
// For the editable brew, if there are multiple sources, present the user with a selection screen. We then filter
|
||||
// the editable brew down to whichever sources they selected.
|
||||
const isChooseSources = brew.head.isEditable && (brew.body._meta?.sources || []).length > 1;
|
||||
|
||||
if (!isChooseSources) {
|
||||
const outFilename = filename || brewName || this.constructor._getBrewName(brew);
|
||||
const json = brew.head.isEditable ? MiscUtil.copyFast(brew.body) : brew.body;
|
||||
this.constructor._mutExportableEditableData({json: json});
|
||||
return DataUtil.userDownload(outFilename, json, {isSkipAdditionalMetadata: true});
|
||||
}
|
||||
|
||||
// region Get chosen sources
|
||||
const getSourceAsText = source => `[${(source.abbreviation || "").qq()}] ${(source.full || "").qq()}`;
|
||||
|
||||
const choices = await InputUiUtil.pGetUserMultipleChoice({
|
||||
title: `Choose Sources`,
|
||||
values: brew.body._meta.sources,
|
||||
fnDisplay: getSourceAsText,
|
||||
isResolveItems: true,
|
||||
max: Number.MAX_SAFE_INTEGER,
|
||||
isSearchable: true,
|
||||
fnGetSearchText: getSourceAsText,
|
||||
});
|
||||
if (choices == null || choices.length === 0) return;
|
||||
// endregion
|
||||
|
||||
// region Filter output by selected sources
|
||||
const cpyBrew = MiscUtil.copyFast(brew.body);
|
||||
const sourceAllowlist = new Set(choices.map(it => it.json));
|
||||
|
||||
cpyBrew._meta.sources = cpyBrew._meta.sources.filter(it => sourceAllowlist.has(it.json));
|
||||
|
||||
Object.entries(cpyBrew)
|
||||
.forEach(([k, v]) => {
|
||||
if (!v || !(v instanceof Array)) return;
|
||||
if (k.startsWith("_")) return;
|
||||
cpyBrew[k] = v.filter(it => {
|
||||
const source = SourceUtil.getEntitySource(it);
|
||||
if (!source) return true;
|
||||
return sourceAllowlist.has(source);
|
||||
});
|
||||
});
|
||||
// endregion
|
||||
|
||||
const reducedFilename = filename || this.constructor._getBrewName({body: cpyBrew});
|
||||
|
||||
this.constructor._mutExportableEditableData({json: cpyBrew});
|
||||
|
||||
return DataUtil.userDownload(reducedFilename, cpyBrew, {isSkipAdditionalMetadata: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* The editable brew may contain `uniqueId` references from the builder, which should be stripped before export.
|
||||
*/
|
||||
static _mutExportableEditableData ({json}) {
|
||||
Object.values(json)
|
||||
.forEach(arr => {
|
||||
if (arr == null || !(arr instanceof Array)) return;
|
||||
arr.forEach(ent => delete ent.uniqueId);
|
||||
});
|
||||
return json;
|
||||
}
|
||||
|
||||
static _getBrewJsonTitle ({brew, brewName}) {
|
||||
brewName = brewName || this._getBrewName(brew);
|
||||
return brew.head.filename || brewName;
|
||||
}
|
||||
|
||||
_pRender_doViewBrew ({evt, brew, brewName}) {
|
||||
const title = this.constructor._getBrewJsonTitle({brew, brewName});
|
||||
const $content = Renderer.hover.$getHoverContent_statsCode(brew.body, {isSkipClean: true, title});
|
||||
Renderer.hover.getShowWindow(
|
||||
$content,
|
||||
Renderer.hover.getWindowPositionFromEvent(evt),
|
||||
{
|
||||
title,
|
||||
isPermanent: true,
|
||||
isBookContent: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async _pRender_pDoOpenBrewMenu ({evt, rdState, brew, brewName, rowMeta}) {
|
||||
rowMeta.menu = rowMeta.menu || this._pRender_getBrewMenu({rdState, brew, brewName});
|
||||
|
||||
await ContextUtil.pOpenMenu(evt, rowMeta.menu);
|
||||
}
|
||||
|
||||
_pRender_getBrewMenu ({rdState, brew, brewName}) {
|
||||
const menuItems = [];
|
||||
|
||||
if (this._isBrewOperationPermitted_update(brew)) {
|
||||
menuItems.push(
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_UPDATE,
|
||||
async () => this._pRender_pDoPullBrew({rdState, brew}),
|
||||
),
|
||||
);
|
||||
} else if (brew.head.isEditable) {
|
||||
menuItems.push(
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_MANAGE_CONTENTS,
|
||||
async () => this._pRender_pDoEditBrew({rdState, brew}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_EXPORT,
|
||||
async () => this._pRender_pDoDownloadBrew({brew, brewName}),
|
||||
),
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_VIEW_JSON,
|
||||
async evt => this._pRender_doViewBrew({evt, brew, brewName}),
|
||||
),
|
||||
);
|
||||
|
||||
if (this._brewUtil.IS_EDITABLE && this._isBrewOperationPermitted_moveToEditable(brew)) {
|
||||
menuItems.push(
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_MOVE_TO_EDITABLE,
|
||||
async () => this._pRender_pDoMoveToEditable({rdState, brews: [brew]}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (this._isBrewOperationPermitted_delete(brew)) {
|
||||
menuItems.push(
|
||||
new ContextUtil.Action(
|
||||
this._LBL_LIST_DELETE,
|
||||
async () => this._pRender_pDoDelete({rdState, brews: [brew]}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ContextUtil.getMenu(menuItems);
|
||||
}
|
||||
|
||||
_pGetUserBoolean_isMoveBrewsToEditable ({brews}) {
|
||||
return InputUiUtil.pGetUserBoolean({
|
||||
title: `Move to Editable ${this._brewUtil.DISPLAY_NAME.toTitleCase()} Document`,
|
||||
htmlDescription: `Moving ${brews.length === 1 ? `this ${this._brewUtil.DISPLAY_NAME}` : `these
|
||||
${this._brewUtil.DISPLAY_NAME_PLURAL}`} to the editable document will prevent ${brews.length === 1 ? "it" : "them"} from being automatically updated in future.<br>Are you sure you want to move ${brews.length === 1 ? "it" : "them"}?`,
|
||||
textYes: "Yes",
|
||||
textNo: "Cancel",
|
||||
});
|
||||
}
|
||||
|
||||
async _pRender_pDoMoveToEditable ({rdState, brews}) {
|
||||
if (!brews?.length) return;
|
||||
|
||||
if (!await this._pGetUserBoolean_isMoveBrewsToEditable({brews})) return;
|
||||
|
||||
await this._brewUtil.pMoveToEditable({brews});
|
||||
|
||||
await this._pRender_pBrewList(rdState);
|
||||
|
||||
JqueryUtil.doToast(`${`${brews.length === 1 ? this._brewUtil.DISPLAY_NAME : this._brewUtil.DISPLAY_NAME_PLURAL}`.uppercaseFirst()} moved to editable document!`);
|
||||
}
|
||||
|
||||
_pGetUserBoolean_isDeleteBrews ({brews}) {
|
||||
if (!brews.some(brew => brew.head.isEditable)) return true;
|
||||
|
||||
const htmlDescription = brews.length === 1
|
||||
? `This document contains all your locally-created or edited ${this._brewUtil.DISPLAY_NAME_PLURAL}.<br>Are you sure you want to delete it?`
|
||||
: `One of the documents you are about to delete contains all your locally-created or edited ${this._brewUtil.DISPLAY_NAME_PLURAL}.<br>Are you sure you want to delete these documents?`;
|
||||
|
||||
return InputUiUtil.pGetUserBoolean({
|
||||
title: `Delete ${this._brewUtil.DISPLAY_NAME}`,
|
||||
htmlDescription,
|
||||
textYes: "Yes",
|
||||
textNo: "Cancel",
|
||||
});
|
||||
}
|
||||
|
||||
async _pRender_pDoDelete ({rdState, brews}) {
|
||||
if (!brews?.length) return;
|
||||
|
||||
if (!await this._pGetUserBoolean_isDeleteBrews({brews})) return;
|
||||
|
||||
await this._brewUtil.pDeleteBrews(brews);
|
||||
|
||||
await this._pRender_pBrewList(rdState);
|
||||
}
|
||||
|
||||
_pRender_getProcBrew (brew) {
|
||||
brew = MiscUtil.copyFast(brew);
|
||||
brew.body._meta.sources.sort((a, b) => SortUtil.ascSortLower(a.full || "", b.full || ""));
|
||||
return brew;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user