"use strict"; class NavBar { static init () { this._initInstallPrompt(); // render the visible elements ASAP window.addEventListener("DOMContentLoaded", NavBar._onDomContentLoaded); window.addEventListener("load", NavBar._onLoad); } static _onDomContentLoaded () { NavBar._initElements(); NavBar.highlightCurrentPage(); } static _onLoad () { NavBar._dropdowns = [...NavBar._navbar.querySelectorAll(`li.dropdown--navbar`)]; document.addEventListener("click", () => NavBar._closeAllDropdowns()); NavBar._clearAllTimers(); NavBar._initAdventureBookElements().then(null); } static _initInstallPrompt () { NavBar._cachedInstallEvent = null; window.addEventListener("beforeinstallprompt", e => NavBar._cachedInstallEvent = e); } static _initElements () { NavBar._navbar = document.getElementById("navbar"); NavBar._tree = new NavBar.Node({ body: NavBar._navbar, }); // create mobile "Menu" button const btnShowHide = document.createElement("button"); btnShowHide.className = "btn btn-default page__btn-toggle-nav"; btnShowHide.innerHTML = "Menu"; btnShowHide.onclick = () => { $(btnShowHide).toggleClass("active"); $(`.page__nav-hidden-mobile`).toggleClass("block", $(btnShowHide).hasClass("active")); }; document.getElementById("navigation").prepend(btnShowHide); this._addElement_li(null, "index.html", "Home", {isRoot: true}); this._addElement_dropdown(null, NavBar._CAT_RULES); this._addElement_li(NavBar._CAT_RULES, "quickreference.html", "Quick Reference"); this._addElement_li(NavBar._CAT_RULES, "variantrules.html", "Optional, Variant, and Expanded Rules"); this._addElement_li(NavBar._CAT_RULES, "tables.html", "Tables"); this._addElement_divider(NavBar._CAT_RULES); this._addElement_dropdown(NavBar._CAT_RULES, NavBar._CAT_BOOKS, {isSide: true, page: "books.html"}); this._addElement_li(NavBar._CAT_BOOKS, "books.html", "View All/Homebrew"); this._addElement_dropdown(null, NavBar._CAT_PLAYER); this._addElement_li(NavBar._CAT_PLAYER, "classes.html", "Classes"); this._addElement_li(NavBar._CAT_PLAYER, "backgrounds.html", "Backgrounds"); this._addElement_li(NavBar._CAT_PLAYER, "feats.html", "Feats"); this._addElement_li(NavBar._CAT_PLAYER, "races.html", "Races"); this._addElement_li(NavBar._CAT_PLAYER, "charcreationoptions.html", "Other Character Creation Options"); this._addElement_li(NavBar._CAT_PLAYER, "optionalfeatures.html", "Other Options & Features"); this._addElement_divider(NavBar._CAT_PLAYER); this._addElement_li(NavBar._CAT_PLAYER, "statgen.html", "Stat Generator"); this._addElement_divider(NavBar._CAT_PLAYER); this._addElement_li(NavBar._CAT_PLAYER, "lifegen.html", "This Is Your Life"); this._addElement_li(NavBar._CAT_PLAYER, "names.html", "Names"); this._addElement_dropdown(null, NavBar._CAT_DUNGEON_MASTER); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "dmscreen.html", "DM Screen"); this._addElement_divider(NavBar._CAT_DUNGEON_MASTER); this._addElement_dropdown(NavBar._CAT_DUNGEON_MASTER, NavBar._CAT_ADVENTURES, {isSide: true, page: "adventures.html"}); this._addElement_li(NavBar._CAT_ADVENTURES, "adventures.html", "View All/Homebrew"); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "cultsboons.html", "Cults & Supernatural Boons"); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "objects.html", "Objects"); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "trapshazards.html", "Traps & Hazards"); this._addElement_divider(NavBar._CAT_DUNGEON_MASTER); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "crcalculator.html", "CR Calculator"); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "encountergen.html", "Encounter Generator"); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "lootgen.html", "Loot Generator"); this._addElement_divider(NavBar._CAT_DUNGEON_MASTER); this._addElement_li(NavBar._CAT_DUNGEON_MASTER, "maps.html", "Maps"); this._addElement_dropdown(null, NavBar._CAT_REFERENCES); this._addElement_li(NavBar._CAT_REFERENCES, "actions.html", "Actions"); this._addElement_li(NavBar._CAT_REFERENCES, "bestiary.html", "Bestiary"); this._addElement_li(NavBar._CAT_REFERENCES, "conditionsdiseases.html", "Conditions & Diseases"); this._addElement_li(NavBar._CAT_REFERENCES, "decks.html", "Decks"); this._addElement_li(NavBar._CAT_REFERENCES, "deities.html", "Deities"); this._addElement_li(NavBar._CAT_REFERENCES, "items.html", "Items"); this._addElement_li(NavBar._CAT_REFERENCES, "languages.html", "Languages"); this._addElement_li(NavBar._CAT_REFERENCES, "rewards.html", "Supernatural Gifts & Rewards"); this._addElement_li(NavBar._CAT_REFERENCES, "psionics.html", "Psionics"); this._addElement_li(NavBar._CAT_REFERENCES, "spells.html", "Spells"); this._addElement_li(NavBar._CAT_REFERENCES, "vehicles.html", "Vehicles"); this._addElement_divider(NavBar._CAT_REFERENCES); this._addElement_li(NavBar._CAT_REFERENCES, "recipes.html", "Recipes"); this._addElement_dropdown(null, NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "search.html", "Search"); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "blocklist.html", "Content Blocklist"); this._addElement_li(NavBar._CAT_UTILITIES, "manageprerelease.html", "Prerelease Content Manager"); this._addElement_li(NavBar._CAT_UTILITIES, "makebrew.html", "Homebrew Builder"); this._addElement_li(NavBar._CAT_UTILITIES, "managebrew.html", "Homebrew Manager"); this._addElement_buttonSplit( NavBar._CAT_UTILITIES, { metas: [ { html: "Load All Partnered Content", click: async evt => { evt.stopPropagation(); evt.preventDefault(); const {ManageBrewUi} = await import("./utils-brew/utils-brew-ui-manage.js"); await ManageBrewUi.pOnClickBtnLoadAllPartnered(); }, }, { html: ``, title: `Export Prerelease Content/Homebrew List as URL`, click: async evt => { evt.stopPropagation(); evt.preventDefault(); const ele = evt.currentTarget; const {ManageBrewUi} = await import("./utils-brew/utils-brew-ui-manage.js"); await ManageBrewUi.pOnClickBtnExportListAsUrl({ele}); }, }, ], }, ); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "inittrackerplayerview.html", "Initiative Tracker Player View"); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "renderdemo.html", "Renderer Demo"); this._addElement_li(NavBar._CAT_UTILITIES, "makecards.html", "RPG Cards JSON Builder"); this._addElement_li(NavBar._CAT_UTILITIES, "converter.html", "Text Converter"); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "plutonium.html", "Plutonium (Foundry Module) Features"); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "https://wiki.tercept.net/en/betteR20", "Roll20 Script Help", {isExternal: true}); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "changelog.html", "Changelog"); this._addElement_li(NavBar._CAT_UTILITIES, NavBar._getCurrentWikiHelpPage(), "Help", {isExternal: true}); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "privacy-policy.html", "Privacy Policy"); this._addElement_dropdown(null, NavBar._CAT_SETTINGS); this._addElement_button( NavBar._CAT_SETTINGS, { html: "Preferences", click: () => { ConfigUi.show(); NavBar._closeAllDropdowns(); }, }, ); this._addElement_divider(NavBar._CAT_SETTINGS); this._addElement_button( NavBar._CAT_SETTINGS, { html: "Save State to File", click: async (evt) => NavBar.InteractionManager._pOnClick_button_saveStateFile(evt), title: "Save any locally-stored data (loaded homebrew, active blocklists, DM Screen configuration,...) to a file.", }, ); this._addElement_button( NavBar._CAT_SETTINGS, { html: "Load State from File", click: async (evt) => NavBar.InteractionManager._pOnClick_button_loadStateFile(evt), title: "Load previously-saved data (loaded homebrew, active blocklists, DM Screen configuration,...) from a file.", }, ); this._addElement_divider(NavBar._CAT_SETTINGS); this._addElement_button( NavBar._CAT_SETTINGS, { html: "Add as App", click: async (evt) => NavBar.InteractionManager._pOnClick_button_addApp(evt), title: "Add the site to your home screen. When used in conjunction with the Preload Offline Data option, this can create a functional offline copy of the site.", }, ); this._addElement_dropdown(NavBar._CAT_SETTINGS, NavBar._CAT_CACHE, {isSide: true}); this._addElement_label(NavBar._CAT_CACHE, `
Preload data for offline use.
Note that visiting a page will automatically preload data for that page.
Note that data which is already preloaded will not be overwritten, unless it is out of date.
`); this._addElement_button( NavBar._CAT_CACHE, { html: "Preload Adventure Text (50MB+)", click: (evt) => NavBar.InteractionManager._pOnClick_button_preloadOffline(evt, {route: /data\/adventure/}), title: "Preload adventure text for offline use.", }, ); this._addElement_button( NavBar._CAT_CACHE, { html: "Preload Book Images (1GB+)", click: (evt) => NavBar.InteractionManager._pOnClick_button_preloadOffline(evt, {route: /img\/book/, isRequireImages: true}), title: "Preload book images offline use. Note that book text is preloaded automatically.", }, ); this._addElement_button( NavBar._CAT_CACHE, { html: "Preload Adventure Text and Images (2GB+)", click: (evt) => NavBar.InteractionManager._pOnClick_button_preloadOffline(evt, {route: /(?:data|img)\/adventure/, isRequireImages: true}), title: "Preload adventure text and images for offline use.", }, ); this._addElement_button( NavBar._CAT_CACHE, { html: "Preload All Images (4GB+)", click: (evt) => NavBar.InteractionManager._pOnClick_button_preloadOffline(evt, {route: /img/, isRequireImages: true}), title: "Preload all images for offline use.", }, ); this._addElement_button( NavBar._CAT_CACHE, { html: "Preload All (5GB+)", click: (evt) => NavBar.InteractionManager._pOnClick_button_preloadOffline(evt, {route: /./, isRequireImages: true}), title: "Preload everything for offline use.", }, ); this._addElement_divider(NavBar._CAT_CACHE); this._addElement_button( NavBar._CAT_CACHE, { html: "Reset Preloaded Data", click: (evt) => NavBar.InteractionManager._pOnClick_button_clearOffline(evt), title: "Remove all preloaded data, and clear away any caches.", }, ); } static _getNode (category) { if (category == null) return NavBar._tree; const _getNodeInner = (level) => { for (const [k, v] of Object.entries(level)) { if (k === category) return v; const subNode = _getNodeInner(v.children); if (subNode) return subNode; } }; return _getNodeInner(NavBar._tree.children); } /** * Adventure/book elements are added as a second, asynchronous, step, as they require loading of: * - An index JSON file. * - The user's Blocklist. */ static async _initAdventureBookElements () { await Promise.all([ PrereleaseUtil.pInit(), BrewUtil2.pInit(), ]); const [adventureBookIndex] = await Promise.all([ DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/gendata-nav-adventure-book-index.json`), ExcludeUtil.pInitialise(), ]); const [prerelease, brew] = await Promise.all([ PrereleaseUtil.pGetBrewProcessed(), BrewUtil2.pGetBrewProcessed(), ]); [ { prop: "book", parentCategory: NavBar._CAT_BOOKS, page: "book.html", fnSort: SortUtil.ascSortBook.bind(SortUtil), }, { prop: "adventure", page: "adventure.html", parentCategory: NavBar._CAT_ADVENTURES, fnSort: SortUtil.ascSortAdventure.bind(SortUtil), }, ].forEach(({prop, parentCategory, page, fnSort}) => { const fromPrerelease = MiscUtil.copyFast(prerelease?.[prop] || []); const fromBrew = MiscUtil.copyFast(brew?.[prop] || []); const mutParentName = (it) => { if (it.parentSource) it.parentName = Parser.sourceJsonToFull(it.parentSource); }; fromPrerelease.forEach(mutParentName); fromBrew.forEach(mutParentName); const metas = [...adventureBookIndex[prop], ...fromPrerelease, ...fromBrew] .filter(it => !ExcludeUtil.isExcluded(UrlUtil.encodeForHash(it.id), prop, it.source, {isNoCount: true})); if (!metas.length) return; SourceUtil.ADV_BOOK_GROUPS .forEach(({group, displayName}) => { const inGroup = metas.filter(it => (it.group || "other") === group); if (!inGroup.length) return; this._addElement_divider(parentCategory); this._addElement_label(parentCategory, displayName, {isAddDateSpacer: true}); const seenYears = new Set(); inGroup .sort(fnSort) .forEach(indexMeta => { const year = indexMeta.published ? (new Date(indexMeta.published).getFullYear()) : null; const isNewYear = year != null && !seenYears.has(year); if (year != null) seenYears.add(year); if (indexMeta.parentSource) { if (!this._getNode(indexMeta.parentName)) { this._addElement_accordion( parentCategory, indexMeta.parentName, { date: isNewYear ? year : null, source: indexMeta.parentSource, isAddDateSpacer: !isNewYear, }, ); } let cleanName = indexMeta.name.startsWith(indexMeta.parentName) ? indexMeta.name.slice(indexMeta.parentName.length).replace(/^:\s+/, "") : indexMeta.name; this._addElement_li( indexMeta.parentName, page, cleanName, { aHash: indexMeta.id, isAddDateSpacer: true, isSide: true, isInAccordion: true, }, ); return; } this._addElement_li( parentCategory, page, indexMeta.name, { aHash: indexMeta.id, date: isNewYear ? year : null, isAddDateSpacer: !isNewYear, source: indexMeta.source, isSide: true, }, ); }); }); }); NavBar.highlightCurrentPage(); } /** * Adds a new item to the navigation bar. Can be used either in root, or in a different UL. * @param parentCategory - Element to append this link to. * @param page - Where does this link to. * @param aText - What text does this link have. * @param [opts] - Options object. * @param [opts.isSide] - True if this item is part of a side menu. * @param [opts.aHash] - Optional hash to be appended to the base href * @param [opts.isRoot] - If the item is a root navbar element. * @param [opts.isExternal] - If the item is an external link. * @param [opts.date] - A date to prefix the list item with. * @param [opts.isAddDateSpacer] - True if this item has no date, but is in a list of items with dates. * @param [opts.source] - A source associated with this item, which should be displayed as a colored marker. * @param [opts.isInAccordion] - True if this item is inside an accordion. * FIXME(Future) this is a bodge; refactor the navbar CSS to avoid using Bootstrap. */ static _addElement_li (parentCategory, page, aText, opts) { opts = opts || {}; const parentNode = this._getNode(parentCategory); const hashPart = opts.aHash ? `#${opts.aHash}`.toLowerCase() : ""; const href = `${page}${hashPart}`; const li = document.createElement("li"); li.setAttribute("role", "presentation"); li.setAttribute("data-page", href); if (opts.isRoot) { li.classList.add("page__nav-hidden-mobile"); li.classList.add("page__btn-nav-root"); } if (opts.isSide) { li.onmouseenter = function () { NavBar._handleSideItemMouseEnter(this); }; } else { li.onmouseenter = function () { NavBar._handleItemMouseEnter(this); }; li.onclick = function () { NavBar._dropdowns.forEach(ele => ele.classList.remove("open")); }; } const a = document.createElement("a"); a.href = href; a.innerHTML = `${this._addElement_getDatePrefix({date: opts.date, isAddDateSpacer: opts.isAddDateSpacer})}${this._addElement_getSourcePrefix({source: opts.source})}${aText}`; a.classList.add("nav__link"); if (opts.isInAccordion) a.classList.add(`nav2-accord__lnk-item`, `inline-block`, `w-100`); if (opts.isExternal) { a.setAttribute("target", "_blank"); a.classList.add("inline-split-v-center"); a.classList.add("w-100"); a.innerHTML = `${aText}`; } li.appendChild(a); parentNode.body.appendChild(li); parentNode.children[href] = new NavBar.NodeLink({ parent: parentNode, head: li, isInAccordion: opts.isInAccordion, lnk: a, }); } static _addElement_accordion ( parentCategory, category, { date = null, isAddDateSpacer = false, source = null, } = {}, ) { const parentNode = this._getNode(parentCategory); const li = document.createElement("li"); li.className = "nav2-accord__wrp"; li.onmouseenter = function () { NavBar._handleItemMouseEnter(this); }; // region Header button const wrpHead = document.createElement("div"); wrpHead.className = "nav2-accord__head split-v-center clickable"; wrpHead.onclick = evt => { evt.stopPropagation(); evt.preventDefault(); node.isExpanded = !node.isExpanded; }; const dispText = document.createElement("div"); dispText.innerHTML = `${this._addElement_getDatePrefix({date, isAddDateSpacer})}${this._addElement_getSourcePrefix({source})}${category}`; const dispToggle = document.createElement("div"); dispToggle.textContent = NavBar.NodeAccordion.getDispToggleDisplayHtml(false); wrpHead.appendChild(dispText); wrpHead.appendChild(dispToggle); // endregion // region Body list const wrpBody = document.createElement("div"); wrpBody.className = `nav2-accord__body ve-hidden`; wrpBody.onclick = function (event) { event.stopPropagation(); }; // endregion li.appendChild(wrpHead); li.appendChild(wrpBody); parentNode.body.appendChild(li); const node = new NavBar.NodeAccordion({ parent: parentNode, head: wrpHead, body: wrpBody, dispToggle, }); parentNode.children[category] = node; } static _addElement_getDatePrefix ({date, isAddDateSpacer}) { return `${(date != null || isAddDateSpacer) ? `