`);
const $tabBar = $(``).appendTo(this._$menuInner);
this.$tabView = $(``).appendTo(this._$menuInner);
this.tabs.forEach(t => {
t.render();
const $head = $(``).appendTo($tabBar);
const $body = $(`
`).appendTo($tabBar);
$body.append(t.get$Tab);
t.$head = $head;
t.$body = $body;
$head.on("click", () => this.setActiveTab(t));
});
}
}
setPanel (pnl) {
this.pnl = pnl;
}
doClose () {
if (this._doClose) this._doClose();
}
doOpen () {
const {$modalInner, doClose} = UiUtil.getShowModal({
cbClose: () => {
this._$menuInner.detach();
// undo entering "tabbed mode" if we close without adding a tab
if (this.pnl.isTabs && this.pnl.tabDatas.filter(it => !it.isDeleted).length === 1) {
this.pnl.setIsTabs(false);
}
},
zIndex: VeCt.Z_INDEX_BENEATH_HOVER,
});
this._doClose = doClose;
$modalInner.append(this._$menuInner);
}
}
class AddMenuTab {
constructor ({board, label}) {
this._board = board;
this.label = label;
this.$tab = null;
this.menu = null;
}
get$Tab () {
return this.$tab;
}
genTabId (type) {
return `tab-${type}-${this.label.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "_")}`;
}
setMenu (menu) {
this.menu = menu;
}
}
class AddMenuVideoTab extends AddMenuTab {
constructor ({...opts}) {
super({...opts, label: "Embed"});
this.tabId = this.genTabId("tube");
}
render () {
if (!this.$tab) {
const $tab = $(`
`);
const $wrpYT = $(`
`).appendTo($tab);
const $iptUrlYT = $(`
`)
.on("keydown", (e) => {
if (e.which === 13) $btnAddYT.click();
})
.appendTo($wrpYT);
const $btnAddYT = $(`
`).appendTo($wrpYT);
$btnAddYT.on("click", () => {
let url = $iptUrlYT.val().trim();
const m = /https?:\/\/(www\.)?youtube\.com\/watch\?v=(.*?)(&.*$|$)/.exec(url);
if (url && m) {
url = `https://www.youtube.com/embed/${m[2]}`;
this.menu.pnl.doPopulate_YouTube(url);
this.menu.doClose();
$iptUrlYT.val("");
} else {
JqueryUtil.doToast({
content: `Please enter a URL of the form: "https://www.youtube.com/watch?v=XXXXXXX"`,
type: "danger",
});
}
});
const $wrpTwitch = $(`
`).appendTo($tab);
const $iptUrlTwitch = $(`
`)
.on("keydown", (e) => {
if (e.which === 13) $btnAddTwitch.click();
})
.appendTo($wrpTwitch);
const $btnAddTwitch = $(`
`).appendTo($wrpTwitch);
const $btnAddTwitchChat = $(`
`).appendTo($wrpTwitch);
const getTwitchM = (url) => {
return /https?:\/\/(www\.)?twitch\.tv\/(.*?)(\?.*$|$)/.exec(url);
};
$btnAddTwitch.on("click", () => {
let url = $iptUrlTwitch.val().trim();
const m = getTwitchM(url);
if (url && m) {
url = `http://player.twitch.tv/?channel=${m[2]}`;
this.menu.pnl.doPopulate_Twitch(url);
this.menu.doClose();
$iptUrlTwitch.val("");
} else {
JqueryUtil.doToast({
content: `Please enter a URL of the form: "https://www.twitch.tv/XXXXXX"`,
type: "danger",
});
}
});
$btnAddTwitchChat.on("click", () => {
let url = $iptUrlTwitch.val().trim();
const m = getTwitchM(url);
if (url && m) {
url = `https://www.twitch.tv/embed/${m[2]}/chat`;
this.menu.pnl.doPopulate_TwitchChat(url);
this.menu.doClose();
$iptUrlTwitch.val("");
} else {
JqueryUtil.doToast({
content: `Please enter a URL of the form: "https://www.twitch.tv/XXXXXX"`,
type: "danger",
});
}
});
const $wrpGeneric = $(`
`).appendTo($tab);
const $iptUrlGeneric = $(`
`)
.on("keydown", (e) => {
if (e.which === 13) $iptUrlGeneric.click();
})
.appendTo($wrpGeneric);
const $btnAddGeneric = $(`
`).appendTo($wrpGeneric);
$btnAddGeneric.on("click", () => {
let url = $iptUrlGeneric.val().trim();
if (url) {
this.menu.pnl.doPopulate_GenericEmbed(url);
this.menu.doClose();
} else {
JqueryUtil.doToast({
content: `Please enter a URL!`,
type: "danger",
});
}
});
this.$tab = $tab;
}
}
}
class AddMenuImageTab extends AddMenuTab {
constructor ({...opts}) {
super({...opts, label: "Image"});
this.tabId = this.genTabId("image");
}
render () {
if (!this.$tab) {
const $tab = $(`
`);
// region Imgur
const $wrpImgur = $(`
`).appendTo($tab);
$(`
Imgur (Anonymous Upload) (accepts imgur-friendly formats)`).appendTo($wrpImgur);
const $iptFile = $(`
`).on("change", (evt) => {
const input = evt.target;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.replace(/.*,/, "");
$.ajax({
url: "https://api.imgur.com/3/image",
type: "POST",
data: {
image: base64,
type: "base64",
},
headers: {
Accept: "application/json",
Authorization: `Client-ID ${IMGUR_CLIENT_ID}`,
},
success: (data) => {
this.menu.pnl.doPopulate_Image(data.data.link, ix);
},
error: (error) => {
try {
JqueryUtil.doToast({
content: `Failed to upload: ${JSON.parse(error.responseText).data.error}`,
type: "danger",
});
} catch (e) {
JqueryUtil.doToast({
content: "Failed to upload: Unknown error",
type: "danger",
});
setTimeout(() => { throw e; });
}
this.menu.pnl.doPopulate_Empty(ix);
},
});
};
reader.onerror = () => {
this.menu.pnl.doPopulate_Empty(ix);
};
reader.fileName = input.files[0].name;
reader.readAsDataURL(input.files[0]);
const ix = this.menu.pnl.doPopulate_Loading("Uploading"); // will be null if not in tabbed mode
this.menu.doClose();
}).appendTo($tab);
const $btnAdd = $(`
`).appendTo($wrpImgur);
$btnAdd.on("click", () => {
$iptFile.click();
});
// endregion
// region URL
const $wrpUtl = $(`
`).appendTo($tab);
const $iptUrl = $(`
`)
.on("keydown", (e) => {
if (e.which === 13) $btnAddUrl.click();
})
.appendTo($wrpUtl);
const $btnAddUrl = $(`
`).appendTo($wrpUtl);
$btnAddUrl.on("click", () => {
let url = $iptUrl.val().trim();
if (url) {
this.menu.pnl.doPopulate_Image(url);
this.menu.doClose();
} else {
JqueryUtil.doToast({
content: `Please enter a URL!`,
type: "danger",
});
}
});
// endregion
$(`
`).appendTo($tab);
// region Adventure dynamic viewer
const $btnSelectAdventure = $(`
`)
.click(() => DmMapper.pHandleMenuButtonClick(this.menu));
$$`
Adventure/Book Map Dynamic Viewer
${$btnSelectAdventure}
`.appendTo($tab);
// endregion
this.$tab = $tab;
}
}
}
class AddMenuSpecialTab extends AddMenuTab {
constructor ({...opts}) {
super({...opts, label: "Special"});
this.tabId = this.genTabId("special");
}
render () {
if (!this.$tab) {
const $tab = $(`
`);
const $wrpRoller = $(`
Dice Roller (pins the existing dice roller to a panel)
`).appendTo($tab);
const $btnRoller = $(`
`).appendTo($wrpRoller);
$btnRoller.on("click", () => {
Renderer.dice.bindDmScreenPanel(this.menu.pnl);
this.menu.doClose();
});
$(`
`).appendTo($tab);
const $btnTracker = $(`
`)
.on("click", async () => {
const pcm = new PanelContentManager_InitiativeTracker({board: this._board, panel: this.menu.pnl});
this.menu.doClose();
await pcm.pDoPopulate();
});
$$`
Initiative Tracker
${$btnTracker}
`.appendTo($tab);
const $btnTrackerCreatureViewer = $(`
`)
.on("click", async () => {
const pcm = new PanelContentManager_InitiativeTrackerCreatureViewer({board: this._board, panel: this.menu.pnl});
this.menu.doClose();
await pcm.pDoPopulate();
});
$$`
Initiative Tracker Creature Viewer
${$btnTrackerCreatureViewer}
`.appendTo($tab);
const $btnPlayerTrackerV1 = $(`
`)
.on("click", async () => {
const pcm = new PanelContentManager_InitiativeTrackerPlayerViewV1({board: this._board, panel: this.menu.pnl});
this.menu.doClose();
await pcm.pDoPopulate();
});
$$`
Initiative Tracker Player View (Standard)
${$btnPlayerTrackerV1}
`.appendTo($tab);
const $btnPlayerTrackerV0 = $(`
`)
.on("click", async () => {
const pcm = new PanelContentManager_InitiativeTrackerPlayerViewV0({board: this._board, panel: this.menu.pnl});
this.menu.doClose();
await pcm.pDoPopulate();
});
$$`
Initiative Tracker Player View (Manual/Legacy)
${$btnPlayerTrackerV0}
`.appendTo($tab);
$(`
`).appendTo($tab);
const $btnSublist = $(`
`)
.click(async evt => {
await this.menu.pnl.pDoMassPopulate_Entities(evt);
this.menu.doClose();
});
$$`
Pinned List Entries
${$btnSublist}
`.appendTo($tab);
$(`
`).appendTo($tab);
const $btnSwitchToEmbedTag = $(`
`)
.click(() => {
this.menu.setActiveTab(this.menu.getTab({label: "Embed"}));
});
const $wrpText = $$`
Basic Text Box (for a feature-rich editor, ${$btnSwitchToEmbedTag} a Google Doc or similar)
`.appendTo($tab);
const $btnText = $(`
`).appendTo($wrpText);
$btnText.on("click", () => {
this.menu.pnl.doPopulate_TextBox();
this.menu.doClose();
});
$(`
`).appendTo($tab);
const $wrpUnitConverter = $(`
Unit Converter
`).appendTo($tab);
const $btnUnitConverter = $(`
`).appendTo($wrpUnitConverter);
$btnUnitConverter.on("click", () => {
this.menu.pnl.doPopulate_UnitConverter();
this.menu.doClose();
});
const $wrpMoneyConverter = $(`
Coin Converter
`).appendTo($tab);
const $btnMoneyConverter = $(`
`).appendTo($wrpMoneyConverter);
$btnMoneyConverter.on("click", () => {
this.menu.pnl.doPopulate_MoneyConverter();
this.menu.doClose();
});
const $wrpCounter = $(`
Counter
`).appendTo($tab);
const $btnCounter = $(`
`).appendTo($wrpCounter);
$btnCounter.on("click", () => {
this.menu.pnl.doPopulate_Counter();
this.menu.doClose();
});
$(`
`).appendTo($tab);
const $wrpTimeTracker = $(`
In-Game Clock/Calendar
`).appendTo($tab);
const $btnTimeTracker = $(`
`).appendTo($wrpTimeTracker);
$btnTimeTracker.on("click", () => {
this.menu.pnl.doPopulate_TimeTracker();
this.menu.doClose();
});
$(`
`).appendTo($tab);
const $wrpBlank = $(`
Blank Space
`).appendTo($tab);
$(`
`)
.on("click", () => {
this.menu.pnl.doPopulate_Blank();
this.menu.doClose();
})
.appendTo($wrpBlank);
this.$tab = $tab;
}
}
}
class AddMenuSearchTab extends AddMenuTab {
static _getTitle (subType) {
switch (subType) {
case "content": return "Content";
case "rule": return "Rules";
case "adventure": return "Adventures";
case "book": return "Books";
default: throw new Error(`Unhandled search tab subtype: "${subType}"`);
}
}
/**
* @param {?object} indexes
* @param {?string} subType
* @param {?object} adventureOrBookIdToSource
* @param opts
*/
constructor ({indexes, subType = "content", adventureOrBookIdToSource = null, ...opts}) {
super({...opts, label: AddMenuSearchTab._getTitle(subType)});
this.tabId = this.genTabId(subType);
this.indexes = indexes;
this.cat = "ALL";
this.subType = subType;
this._adventureOrBookIdToSource = adventureOrBookIdToSource;
this.$selCat = null;
this.$srch = null;
this.$results = null;
this.showMsgIpt = null;
this.doSearch = null;
this._$ptrRows = null;
}
_getSearchOptions () {
switch (this.subType) {
case "content": return {
fields: {
n: {boost: 5, expand: true},
s: {expand: true},
},
bool: "AND",
expand: true,
};
case "rule": return {
fields: {
h: {boost: 5, expand: true},
s: {expand: true},
},
bool: "AND",
expand: true,
};
case "adventure":
case "book": return {
fields: {
c: {boost: 5, expand: true},
n: {expand: true},
},
bool: "AND",
expand: true,
};
default: throw new Error(`Unhandled search tab subtype: "${this.subType}"`);
}
}
_$getRow (r) {
switch (this.subType) {
case "content": return $(`
${r.doc.cf} ${r.doc.n}
${r.doc.s ? `${Parser.sourceJsonToAbv(r.doc.s)}${r.doc.p ? ` p${r.doc.p}` : ""}` : ""}
`);
case "rule": return $(`
${r.doc.h}
${r.doc.n}, ${r.doc.s}
`);
case "adventure":
case "book": return $(`
${r.doc.c}
${r.doc.n}${r.doc.o ? `, ${r.doc.o}` : ""}
`);
default: throw new Error(`Unhandled search tab subtype: "${this.subType}"`);
}
}
_getAllTitle () {
switch (this.subType) {
case "content": return "All Categories";
case "rule": return "All Categories";
case "adventure": return "All Adventures";
case "book": return "All Books";
default: throw new Error(`Unhandled search tab subtype: "${this.subType}"`);
}
}
_getCatOptionText (key) {
switch (this.subType) {
case "content": return key;
case "rule": return key;
case "adventure":
case "book": {
key = (this._adventureOrBookIdToSource[this.subType] || {})[key] || key; // map the key (an adventure/book id) to its source if possible
return Parser.sourceJsonToFull(key);
}
default: throw new Error(`Unhandled search tab subtype: "${this.subType}"`);
}
}
render () {
const flags = {
doClickFirst: false,
isWait: false,
};
this.showMsgIpt = () => {
flags.isWait = true;
this.$results.empty().append(SearchWidget.getSearchEnter());
};
const showMsgDots = () => {
this.$results.empty().append(SearchWidget.getSearchLoading());
};
const showNoResults = () => {
flags.isWait = true;
this.$results.empty().append(SearchWidget.getSearchEnter());
};
this._$ptrRows = {_: []};
this.doSearch = () => {
const srch = this.$srch.val().trim();
const searchOptions = this._getSearchOptions();
const index = this.indexes[this.cat];
const results = index.search(srch, searchOptions);
const resultCount = results.length ? results.length : index.documentStore.length;
const toProcess = results.length ? results : Object.values(index.documentStore.docs).slice(0, UiUtil.SEARCH_RESULTS_CAP).map(it => ({doc: it}));
this.$results.empty();
this._$ptrRows._ = [];
if (toProcess.length) {
const handleClick = (r) => {
switch (this.subType) {
case "content": {
const page = UrlUtil.categoryToHoverPage(r.doc.c);
const source = r.doc.s;
const hash = r.doc.u;
this.menu.pnl.doPopulate_Stats(page, source, hash);
break;
}
case "rule": {
this.menu.pnl.doPopulate_Rules(r.doc.b, r.doc.p, r.doc.h);
break;
}
case "adventure": {
this.menu.pnl.doPopulate_Adventures(r.doc.a, r.doc.p);
break;
}
case "book": {
this.menu.pnl.doPopulate_Books(r.doc.b, r.doc.p);
break;
}
default: throw new Error(`Unhandled search tab subtype: "${this.subType}"`);
}
this.menu.doClose();
};
if (flags.doClickFirst) {
handleClick(toProcess[0]);
flags.doClickFirst = false;
return;
}
const res = toProcess.slice(0, UiUtil.SEARCH_RESULTS_CAP);
res.forEach(r => {
const $row = this._$getRow(r).appendTo(this.$results);
SearchWidget.bindRowHandlers({result: r, $row, $ptrRows: this._$ptrRows, fnHandleClick: handleClick, $iptSearch: this.$srch});
this._$ptrRows._.push($row);
});
if (resultCount > UiUtil.SEARCH_RESULTS_CAP) {
const diff = resultCount - UiUtil.SEARCH_RESULTS_CAP;
this.$results.append(`
...${diff} more result${diff === 1 ? " was" : "s were"} hidden. Refine your search!
`);
}
} else {
if (!srch.trim()) this.showMsgIpt();
else showNoResults();
}
};
if (!this.$tab) {
const $tab = $(`
`);
const $wrpCtrls = $(`
`).appendTo($tab);
const $selCat = $(`
`).appendTo($wrpCtrls).toggle(Object.keys(this.indexes).length !== 1);
Object.keys(this.indexes).sort().filter(it => it !== "ALL").forEach(it => {
$selCat.append(`
`);
});
$selCat.on("change", () => {
this.cat = $selCat.val();
this.doSearch();
});
const $srch = $(`
`).blurOnEsc().appendTo($wrpCtrls);
const $results = $(`
`).appendTo($tab);
SearchWidget.bindAutoSearch($srch, {
flags,
fnSearch: this.doSearch,
fnShowWait: showMsgDots,
$ptrRows: this._$ptrRows,
});
this.$tab = $tab;
this.$selCat = $selCat;
this.$srch = $srch;
this.$results = $results;
this.doSearch();
}
}
doTransitionActive () {
this.$srch.val("").focus();
if (this.doSearch) this.doSearch();
}
}
class RuleLoader {
static async pFill (book) {
const $$$ = RuleLoader.cache;
if ($$$[book]) return $$$[book];
const data = await DataUtil.loadJSON(`data/generated/${book}.json`);
Object.keys(data.data).forEach(b => {
const ref = data.data[b];
if (!$$$[b]) $$$[b] = {};
ref.forEach((c, i) => {
if (!$$$[b][i]) $$$[b][i] = {};
c.entries.forEach(s => {
$$$[b][i][s.name] = s;
});
});
});
}
static getFromCache (book, chapter, header) {
return RuleLoader.cache[book][chapter][header];
}
}
RuleLoader.cache = {};
class AdventureOrBookLoader {
constructor (type) {
this._type = type;
this._cache = {};
this._pLoadings = {};
this._availableOfficial = new Set();
this._indexOfficial = null;
}
async pInit () {
const indexPath = this._getIndexPath();
this._indexOfficial = await DataUtil.loadJSON(indexPath);
this._indexOfficial[this._type].forEach(meta => this._availableOfficial.add(meta.id.toLowerCase()));
}
_getIndexPath () {
switch (this._type) {
case "adventure": return `${Renderer.get().baseUrl}data/adventures.json`;
case "book": return `${Renderer.get().baseUrl}data/books.json`;
default: throw new Error(`Unknown loader type "${this._type}"`);
}
}
_getJsonPath (bookOrAdventure) {
switch (this._type) {
case "adventure": return `${Renderer.get().baseUrl}data/adventure/adventure-${bookOrAdventure.toLowerCase()}.json`;
case "book": return `${Renderer.get().baseUrl}data/book/book-${bookOrAdventure.toLowerCase()}.json`;
default: throw new Error(`Unknown loader type "${this._type}"`);
}
}
async _pGetPrereleaseData ({advBookId, prop}) {
return this._pGetPrereleaseBrewData({advBookId, prop, brewUtil: PrereleaseUtil});
}
async _pGetBrewData ({advBookId, prop}) {
return this._pGetPrereleaseBrewData({advBookId, prop, brewUtil: BrewUtil2});
}
async _pGetPrereleaseBrewData ({advBookId, prop, brewUtil}) {
const searchFor = advBookId.toLowerCase();
const brew = await brewUtil.pGetBrewProcessed();
switch (this._type) {
case "adventure":
case "book": {
return (brew[prop] || []).find(it => it.id.toLowerCase() === searchFor);
}
default: throw new Error(`Unknown loader type "${this._type}"`);
}
}
async pFill (advBookId) {
if (!this._pLoadings[advBookId]) {
this._pLoadings[advBookId] = (async () => {
this._cache[advBookId] = {};
let head, body;
if (this._availableOfficial.has(advBookId.toLowerCase())) {
head = this._indexOfficial[this._type].find(it => it.id.toLowerCase() === advBookId.toLowerCase());
body = await DataUtil.loadJSON(this._getJsonPath(advBookId));
} else {
head = await this._pGetBrewData({advBookId, prop: this._type});
body = await this._pGetBrewData({advBookId, prop: `${this._type}Data`});
}
if (!head || !body) return;
this._cache[advBookId] = {head, chapters: {}};
body.data.forEach((chap, i) => this._cache[advBookId].chapters[i] = chap);
})();
}
await this._pLoadings[advBookId];
}
getFromCache (adventure, chapter, {isAllowMissing = false} = {}) {
const outHead = this._cache?.[adventure]?.head;
const outBody = this._cache?.[adventure]?.chapters?.[chapter];
if (outHead && outBody) return {chapter: outBody, head: outHead};
if (isAllowMissing) return null;
return {chapter: MiscUtil.copy(AdventureOrBookLoader._NOT_FOUND), head: {source: VeCt.STR_GENERIC, id: VeCt.STR_GENERIC}};
}
}
AdventureOrBookLoader._NOT_FOUND = {
type: "section",
name: "(Missing Content)",
entries: [
"The content you attempted to load could not be found. Is it homebrew, and not currently loaded?",
],
};
class AdventureLoader extends AdventureOrBookLoader { constructor () { super("adventure"); } }
class BookLoader extends AdventureOrBookLoader { constructor () { super("book"); } }
const adventureLoader = new AdventureLoader();
const bookLoader = new BookLoader();
class NoteBox {
static make$Notebox (board, content) {
const $iptText = $(`
`)
.on("keydown", async evt => {
const key = EventUtil.getKeyIgnoreCapsLock(evt);
const isCtrlQ = (EventUtil.isCtrlMetaKey(evt)) && key === "q";
if (!isCtrlQ) {
board.doSaveStateDebounced();
return;
}
const txt = $iptText[0];
if (txt.selectionStart === txt.selectionEnd) {
const pos = txt.selectionStart - 1;
const text = txt.value;
const l = text.length;
let beltStack = [];
let braceStack = [];
let belts = 0;
let braces = 0;
let beltsAtPos = null;
let bracesAtPos = null;
let lastBeltPos = null;
let lastBracePos = null;
outer: for (let i = 0; i < l; ++i) {
const c = text[i];
switch (c) {
case "[":
belts = Math.min(belts + 1, 2);
if (belts === 2) beltStack = [];
lastBeltPos = i;
break;
case "]":
belts = Math.max(belts - 1, 0);
if (belts === 0 && i > pos) break outer;
break;
case "{":
if (text[i + 1] === "@") {
braces = 1;
braceStack = [];
lastBracePos = i;
}
break;
case "}":
braces = 0;
if (i >= pos) break outer;
break;
default:
if (belts === 2) {
beltStack.push(c);
}
if (braces) {
braceStack.push(c);
}
}
if (i === pos) {
beltsAtPos = belts;
bracesAtPos = braces;
}
}
if (beltsAtPos === 2 && belts === 0) {
const str = beltStack.join("");
await Renderer.dice.pRoll2(str.replace(`[[`, "").replace(`]]`, ""), {
isUser: false,
name: "DM Screen",
});
} else if (bracesAtPos === 1 && braces === 0) {
const str = braceStack.join("");
const tag = str.split(" ")[0].replace(/^@/, "");
const text = str.split(" ").slice(1).join(" ");
if (Renderer.tag.getPage(tag)) {
const r = Renderer.get().render(`{${str}}`);
evt.type = "mouseover";
evt.shiftKey = true;
evt.ctrlKey = false;
evt.metaKey = false;
$(r).trigger(evt);
} else if (tag === "link") {
const [txt, link] = Renderer.splitTagByPipe(text);
window.open(link && link.trim() ? link : txt);
}
}
}
});
return $iptText;
}
}
class UnitConverter {
static make$Converter (board, state) {
const units = [
new UnitConverterUnit("Inches", "2.54", "Centimetres", "0.394"),
new UnitConverterUnit("Feet", "0.305", "Metres", "3.28"),
new UnitConverterUnit("Miles", "1.61", "Kilometres", "0.620"),
new UnitConverterUnit("Pounds", "0.454", "Kilograms", "2.20"),
new UnitConverterUnit("Gallons", "3.79", "Litres", "0.264"),
new UnitConverterUnit("Gallons", "8", "Pints", "0.125"),
];
let ixConv = state.c || 0;
let dirConv = state.d || 0;
const $wrpConverter = $(`
`);
const $tblConvert = $(`
`).appendTo($wrpConverter);
const $tbodyConvert = $(`
`).appendTo($tblConvert);
units.forEach((u, i) => {
const $tr = $(`
|
`).appendTo($tbodyConvert);
const clickL = () => {
ixConv = i;
dirConv = 0;
updateDisplay();
};
const clickR = () => {
ixConv = i;
dirConv = 1;
updateDisplay();
};
$(`
${u.n1} | `).click(clickL).appendTo($tr);
$(`
×${u.x1.padStart(5)} | `).click(clickL).appendTo($tr);
$(`
${u.n2} | `).click(clickR).appendTo($tr);
$(`
×${u.x2.padStart(5)} | `).click(clickR).appendTo($tr);
});
const $wrpIpt = $(`
`).appendTo($wrpConverter);
const $wrpLeft = $(`
`).appendTo($wrpIpt);
const $lblLeft = $(`
`).appendTo($wrpLeft);
const $iptLeft = $(`
`).appendTo($wrpLeft);
const $btnSwitch = $(`
`).click(() => {
dirConv = Number(!dirConv);
updateDisplay();
}).appendTo($wrpIpt);
const $wrpRight = $(`
`).appendTo($wrpIpt);
const $lblRight = $(`
`).appendTo($wrpRight);
const $iptRight = $(`
`).appendTo($wrpRight);
const updateDisplay = () => {
const it = units[ixConv];
const [lblL, lblR] = dirConv === 0 ? [it.n1, it.n2] : [it.n2, it.n1];
$lblLeft.text(lblL);
$lblRight.text(lblR);
handleInput();
};
const mMaths = /^([0-9.+\-*/ ()e])*$/;
const handleInput = () => {
const showInvalid = () => {
$iptLeft.addClass(`ipt-invalid`);
$iptRight.val("");
};
const showValid = () => {
$iptLeft.removeClass(`ipt-invalid`);
};
const val = ($iptLeft.val() || "").trim();
if (!val) {
showValid();
$iptRight.val("");
} else if (mMaths.exec(val)) {
showValid();
const it = units[ixConv];
const mL = [Number(it.x1), Number(it.x2)][dirConv];
try {
/* eslint-disable */
const total = eval(val);
/* eslint-enable */
$iptRight.val(Number((total * mL).toFixed(5)));
} catch (e) {
$iptLeft.addClass(`ipt-invalid`);
$iptRight.val("");
}
} else showInvalid();
board.doSaveStateDebounced();
};
UiUtil.bindTypingEnd({$ipt: $iptLeft, fnKeyup: handleInput});
updateDisplay();
$wrpConverter.data("getState", () => {
return {
c: ixConv,
d: dirConv,
i: $iptLeft.val(),
};
});
return $wrpConverter;
}
}
class UnitConverterUnit {
constructor (n1, x1, n2, x2) {
this.n1 = n1;
this.x1 = x1;
this.n2 = n2;
this.x2 = x2;
}
}
class AdventureOrBookView {
constructor (prop, panel, loader, tabIx, contentMeta) {
this._prop = prop;
this._panel = panel;
this._loader = loader;
this._tabIx = tabIx;
this._contentMeta = contentMeta;
this._$wrpContent = null;
this._$wrpContentOuter = null;
this._$titlePrev = null;
this._$titleNext = null;
}
$getEle () {
this._$titlePrev = $(`
`);
this._$titleNext = $(`
`);
const $btnPrev = $(`
`)
.click(() => this._handleButtonClick(-1));
const $btnNext = $(`
`)
.click(() => this._handleButtonClick(1));
this._$wrpContent = $(`
`);
this._$wrpContentOuter = $$`
`;
const $wrp = $$`
${this._$wrpContentOuter}
${this._$titlePrev}${$btnPrev}${$btnNext}${this._$titleNext}
`;
// assumes the data has already been loaded/cached
this._render();
return $wrp;
}
_handleButtonClick (direction) {
this._contentMeta.c += direction;
const hasRenderedData = this._render({isSkipMissingData: true});
if (!hasRenderedData) this._contentMeta.c -= direction;
else {
this._$wrpContentOuter.scrollTop(0);
this._panel.board.doSaveStateDebounced();
}
}
_getData (chapter, {isAllowMissing = false} = {}) {
return this._loader.getFromCache(this._contentMeta[this._prop], chapter, {isAllowMissing});
}
static _PROP_TO_URL = {
"a": UrlUtil.PG_ADVENTURE,
"b": UrlUtil.PG_BOOK,
};
_render ({isSkipMissingData = false} = {}) {
const hasData = !!this._getData(this._contentMeta.c, {isAllowMissing: true});
if (!hasData && isSkipMissingData) return false;
const {head, chapter} = this._getData(this._contentMeta.c);
this._panel.setTabTitle(this._tabIx, chapter.name);
const stack = [];
const page = this.constructor._PROP_TO_URL[this._prop];
Renderer
.get()
.setFirstSection(true)
.recursiveRender(
chapter,
stack,
{
adventureBookPage: page,
adventureBookSource: head.source,
adventureBookHash: UrlUtil.URL_TO_HASH_BUILDER[page]({id: this._contentMeta[this._prop]}),
},
);
this._$wrpContent.empty().fastSetHtml(stack[0]);
const dataPrev = this._getData(this._contentMeta.c - 1, {isAllowMissing: true});
const dataNext = this._getData(this._contentMeta.c + 1, {isAllowMissing: true});
this._$titlePrev.text(dataPrev ? dataPrev.name : "").title(dataPrev ? dataPrev.name : "");
this._$titleNext.text(dataNext ? dataNext.name : "").title(dataNext ? dataNext.name : "");
return hasData;
}
}
window.addEventListener("load", () => {
// expose it for dbg purposes
window.DM_SCREEN = new Board();
Renderer.hover.bindDmScreen(window.DM_SCREEN);
window.DM_SCREEN.pInitialise()
.catch(err => {
JqueryUtil.doToast({content: `Failed to load with error "${err.message}". ${VeCt.STR_SEE_CONSOLE}`, type: "danger"});
$(`.dm-screen-loading`).find(`.initial-message`).text("Failed!");
setTimeout(() => { throw err; });
});
});