import { LoadSaveFile } from "./LoadSaveFile.js"; /* ------------------------ Load image files (necessary for Webpack) ---------------------------------------------------------- */ import HEALTH_MASK_IMAGE from "../img/health-mask.png"; import HEALTH_MASK_STEEL_IMAGE from "../img/health-mask-steel.png"; import SOUL_ORB_IMAGE from "../img/soul-orb.png"; import NOTCH_IMAGE from "../img/notch.png"; import NOTCH_FILLED_IMAGE from "../img/notch-filled.png"; import NOTCH_OVERCHARMED_IMAGE from "../img/notch-overcharmed.png"; import GEO_IMAGE from "../img/geo.png"; import GEO_SHADE_IMAGE from "../img/geo-shade.png"; /* ------------------------- Constants ---------------------------------------------------------------------------------------- */ // const DATA_UNKNOWN = "Data unknown"; const SYMBOL_FALSE = ""; // "❌ " const SYMBOL_TRUE = ""; // "✅ " const SYMBOL_PARTIAL = ""; // "✔ " const SYMBOL_CLOCK = ""; // "🕑 " const SYMBOL_FILE = ""; // "📁" const SYMBOL_BINDING_NAIL = ""; // Nail Binding const SYMBOL_BINDING_SHELL = ""; // Shell Binding const SYMBOL_BINDING_CHARMS = ""; // Charm Binding const SYMBOL_BINDING_SOUL = ""; // Soul Binding const SYMBOL_BINDING_ALL = ""; // All Bindings const SYMBOL_ATTUNED = ""; // Attuned const SYMBOL_ASCENDED = ""; // Ascended const SYMBOL_RADIANT = ""; // Radiant const SYMBOL_EMPTY = ""; // No symbol const FLEUR_DIVIDE = "
"; const WIKI_LINK = "https://hollowknight.fandom.com/wiki/"; const ROOT = document.documentElement; const SCROLL_BUTTON = document.querySelector(".scroll-up-button"); /* -------------------------- Variables --------------------------------------------------------------------------------------- */ let benchmarkTimes = { LoadSaveFile: { name: "LoadSaveFile()", timeStart: 0, timeEnd: 0 }, CheckCompletion: { name: "HKCheckCompletion()", timeStart: 0, timeEnd: 0 }, GenerateInnerHTML: { name: "GenerateInnerHTML()", timeStart: 0, timeEnd: 0 }, HKReadTextArea: { name: "HKReadTextArea()", timeStart: 0, timeEnd: 0 }, Total: { name: "Total", timeStart: 0, timeEnd: 0 } }; /* ######################################################################################### */ function Benchmark(bench) { let result = 0; for (let time in bench) { if (bench[time].timeStart !== 0 && bench[time].timeEnd !== 0) { result = bench[time].timeEnd - bench[time].timeStart; console.info(`${bench[time].name} time (ms) = %c${result.toFixed(2)}`, `color: #008cdc; font-weight: 700;`); } bench[time].timeStart = 0; bench[time].timeEnd = 0; } } function ScrollToElement(element) { /* Scroll to the element top */ element.scrollTo({ top: 0, behavior: "smooth" }); } function ShowElement(element) { element.classList.add("visible-block"); element.classList.add("opacity-full"); element.classList.remove("hidden"); } function HideElement(element) { element.classList.remove("visible-block"); element.classList.remove("opacity-full"); element.classList.add("hidden"); } function TogglePageScrollElement(root, element, ratio) { /* Maximum number of pixels that can be scrolled by the user */ let scrollTotal = root.scrollHeight - root.clientHeight; if ((root.scrollTop / scrollTotal) > ratio) { /* Show button */ ShowElement(element); } else { /* Hide button */ HideElement(element); } } /** * Replaces the h2 titles with a current percent/max percent values as read from the save file * @param {object} jsObj Object with HTML data to fill * @param {number} hkGameCompletion Total completion percentage in a save file */ /* function CompletionHTML(jsObj, hkGameCompletion) { let h2 = ""; let h2id = ""; let cl = ""; let clGreen = "box-green"; let clRed = "box-red"; let cp = 0; // current Percent let mp = 0; // max Percent let fillText = ""; for (let i in jsObj) { h2 = jsObj[i].h2; h2id = "h2-" + jsObj[i].id; (jsObj[i].hasOwnProperty("percent")) ? cp = jsObj[i].percent: cp = 0; if (i === "intro") cp = hkGameCompletion; // Don't use percent-box for Essentials, Achievements, Statistics if (!jsObj[i].hasOwnProperty("maxPercent")) { fillText = ""; } // otherwise use percent-box with values cp/mp% else { mp = jsObj[i].maxPercent; if (i === "maskShards") { let perc = jsObj[i].percent; (perc % 4) ? cp = Math.floor(perc / 4): cp = perc / 4; } else if (i === "vesselFragments") { let perc = jsObj[i].percent; (perc % 3) ? cp = Math.floor(perc / 3): cp = perc / 3; } // switches the box to red when a section (h2) is 0 if (cp === 0) { cl = ` ${clRed}`; } // switches the box to green when a section (h2) is completed else if (cp === mp) { cl = ` ${clGreen}`; } // default is blue (partially completed and starting value) else cl = ""; // needed for Game Status to show percentage properly (adds a slash for all boxes except the Game Status one) if (jsObj[i].id != "hk-intro") cp += "/"; fillText = `
${(i === "intro") ? cp: cp + jsObj[i].maxPercent}%
`; } } } */ /* ################################### Optimized Functions ########################################################################## */ function GenerateInnerHTML(db) { // start benchmarking benchmarkTimes.GenerateInnerHTML.timeStart = performance.now(); let sections = db.sections; /* console.log(sections); */ let entries = {}; let obj = { icon: "", iconClass: "", textPrefix: "", textSuffix: "", wiki: "", div: `
`, b: ["", ""], p: "", span: ["", ""], spoiler: ["", ""], spoilerAfter: "", }; let finalHTMLFill = ""; let textFill = ""; let Img = ""; let maskNormal = `health mask image`; let maskSteel = `steel health mask image`; let soulNormal = `soul orb image`; let notchNormalImage = `notch image`; let notchFilledImage = `notch image`; let notchOvercharmedImage = `notch image`; let geoNormalImage = `geo symbol image`; let geoShadeImage = `shade geo symbol image`; let div = `
`; let divFlex = `
`; /* ############################## create all main entries ########################################################################## */ for (let section in sections) { textFill = ""; /* ############################# Tab Switch buttons and Large Section
s for switching ############################## */ /* Important: Ending Tab
s are at "End the Tab divs" */ switch (section) { /* Main % */ case "bosses": textFill += [ `
`, ``, ``, ``, ``, ``, ``, ``, ``, `
`, ].join("\n"); textFill += `
`; break; /* Essentials % */ case "essential": textFill += `
`; break; /* Journal */ case "huntersJournal": textFill += `
`; break; /* Collectibles */ case "charmNotches": textFill += `
`; break; /* Geo Caches */ case "geoChests": textFill += `
`; break; /* Secrets */ case "worldInteractions": textFill += `
`; break; /* Statistics */ case "statistics": textFill += `
`; break; /* Godmaster */ case "godhomeStatistics": textFill += `
`; break; } entries = sections[section].entries; /* ####################### Section div id start ########################## */ /* starts a new
with the current section id */ textFill += SectionStart(sections[section]); /* creates a

tag for the current section and fills with current%/max% If the save file was not analyzed, then fill only max% on blue background */ if (db.saveAnalyzed === true) { textFill += CompletionFill(sections[section]); } else { textFill += CompletionFillNoSave(sections[section]); } /* ######################## Different behaviour depending on the section ###################################################### */ switch (section) { /* ############### Game Status (intro) ################ */ case "intro": /* ############## Create each single entry (intro) ############### */ for (let entry in entries) { obj.b = ["", ""]; obj.p = ""; obj.span = ["", ""]; obj.div = div; /* -------- Icons (next to each entry) --------- */ if (entries[entry].hasOwnProperty("icon")) { switch (entries[entry].icon) { case "clock": obj.icon = SYMBOL_CLOCK; break; case "green": obj.icon = SYMBOL_TRUE; break; case "partial": obj.icon = SYMBOL_PARTIAL; break; case "revealed": obj.icon = SYMBOL_EMPTY; break; case "red": obj.icon = SYMBOL_FALSE; break; default: obj.icon = SYMBOL_EMPTY; } } else { obj.icon = SYMBOL_FALSE; } obj.textPrefix = entries[entry].name; obj.textSuffix = entries[entry].spoiler; obj.spoilerAfter = ""; /* Different text and images for each entry in the "Game Status" (intro) section */ switch (entry) { case "gameCompletion": obj.textSuffix = `${obj.textSuffix} %`; obj.spoilerAfter = ` ${entries[entry].spoilerAfter}`; obj.span[1] = ""; break; case "gameCompletionExtended": obj.spoilerAfter = ` ${entries[entry].spoilerAfter}`; obj.span[1] = ""; break; case "health": /* ----------------- Horizontal Line after save version ---------------- */ textFill += FLEUR_DIVIDE; obj.div = divFlex; obj.span = ["", ""]; (entries[entry].permadeathMode) ? Img = maskSteel: Img = maskNormal; for (let i = 0, total = entries[entry].amountTotal; i < total; i++) { obj.textSuffix += Img; } obj.textSuffix += `${obj.p}(${entries[entry].amountTotal})`; obj.p = ""; break; case "soul": obj.div = divFlex; obj.span = ["", ""]; Img = soulNormal; for (let i = 0, total = Math.round(entries[entry].amountTotal / 33); i < total; i++) { obj.textSuffix += Img; } obj.textSuffix += `${obj.p}(${Math.round(entries[entry].amountTotal / 33)})`; obj.p = ""; break; case "notches": obj.div = divFlex; obj.span = ["", ""]; /* First, check filled (used) notches and fill them (skips if no filled notches) */ if (entries[entry].amountFilled > 0) { for (let i = 0, total = entries[entry].amountFilled; i < total; i++) { obj.textSuffix += notchFilledImage; } } /* Second, check overcharmed notches and fill them (skips if player is not overcharmed) */ if (entries[entry].amountOvercharmed > 0) { for (let i = 0, total = entries[entry].amountOvercharmed; i < total; i++) { obj.textSuffix += notchOvercharmedImage; } } /* Last, fill all unused notches */ if (entries[entry].amountUnused > 0) { for (let i = 0, total = entries[entry].amountUnused; i < total; i++) { obj.textSuffix += notchNormalImage; } } obj.textSuffix += `${obj.p}(${entries[entry].amountTotal})`; break; case "geo": obj.div = divFlex; obj.span = ["", ""]; obj.textSuffix += `${geoNormalImage}${entries[entry].amount}`; // Show Shade Geo value and image only if Shade has at least 1 Geo on it if (entries[entry].amountShade > 0) obj.textSuffix += `${obj.p}+${geoShadeImage}${entries[entry].amountShade}`; // Show also total Geo (Geo + Shade Geo) if player has at least 1 geo alongside the shade geo if (entries[entry].amount > 0 && entries[entry].amountShade > 0) { obj.textSuffix += `${obj.p}=${obj.p}${entries[entry].amountTotal}`; } obj.p = ""; break; default: } textFill += SingleEntryFill(obj); } break; /* #################### Hints (hints) #################### */ case "hints": obj.b = ["", ""]; obj.span = ["", ""]; obj.icon = ""; obj.textPrefix = ""; obj.div = div; /* display only one (current) hint */ obj.textSuffix = entries[sections[section].current].spoiler; textFill += SingleEntryFill(obj); break; /* ######################### Create all other sections ################################################################## */ default: /* ###################### Create section descriptions under each H2 title ##################### */ if (sections[section].hasOwnProperty("description")) { textFill += SectionDescription(sections[section]); } /* ###################### Create each single entry (from all other sections) ##################### */ for (let entry in entries) { /* obj.p = ""; */ obj.p = ""; obj.span = ["", ""]; obj.spoiler = ["", ""]; obj.div = div; obj.textPrefix = entries[entry].name; obj.textSuffix = `— ${entries[entry].spoiler}`; obj.wiki = entries[entry].wiki; /* -------- Icons (next to each entry) --------- */ if (entries[entry].hasOwnProperty("icon")) { switch (entries[entry].icon) { case "clock": obj.icon = SYMBOL_CLOCK; break; case "green": obj.icon = SYMBOL_TRUE; /* -------- Prevents hiding as spoiler when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "partial": obj.icon = SYMBOL_PARTIAL; break; case "revealed": obj.icon = SYMBOL_EMPTY; /* -------- Prevents textSuffix blurring when a player has already discovered the entry --------- */ obj.span[0] = ""; break; case "partialJournal": obj.icon = SYMBOL_PARTIAL; /* -------- Prevents textSuffix blurring when a player has already discovered the entry --------- */ obj.span[0] = ""; break; case "bindingNail": obj.icon = SYMBOL_BINDING_NAIL; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "bindingShell": obj.icon = SYMBOL_BINDING_SHELL; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "bindingCharms": obj.icon = SYMBOL_BINDING_CHARMS; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "bindingSoul": obj.icon = SYMBOL_BINDING_SOUL; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "bindingAll": obj.icon = SYMBOL_BINDING_ALL; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "attuned": obj.icon = SYMBOL_ATTUNED; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "ascended": obj.icon = SYMBOL_ASCENDED; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "radiant": obj.icon = SYMBOL_RADIANT; /* -------- Prevents blurring when a player has already completed the entry --------- */ obj.span[0] = ""; break; case "red": obj.icon = SYMBOL_FALSE; break; default: obj.icon = SYMBOL_EMPTY; } } else { obj.icon = SYMBOL_FALSE; } /* assign the appropriate spoiler class name depending on the completion check (for blurring names) */ if (entries[entry].hasOwnProperty("icon")) { switch (entries[entry].icon) { case "red": case "none": obj.iconClass = " spoiler-red blurred"; break; default: /* -------- Prevents TextPrefix blurring when a player has already discovered the entry --------- */ obj.iconClass = ""; } } else { obj.iconClass = ""; } obj.b = [``, ""]; if (entries[entry].hasOwnProperty("amount")) { if (entries[entry].hasOwnProperty("disabled")) { if (entries[entry].disabled !== true) { obj.textPrefix += `: ${entries[entry].amount}`; } } else { obj.textPrefix += `: ${entries[entry].amount}`; } } if (entries[entry].hasOwnProperty("amountTotal")) { obj.textPrefix += ` / ${entries[entry].amountTotal}`; } if (entries[entry].hasOwnProperty("id")) { switch (entries[entry].id) { case "geoRocks": case "itemsDiscovered": obj.textPrefix += `: ${entries[entry].notActivated} | ${entries[entry].activated} | ${entries[entry].discoveredTotal}`; break; default: } } if (entries[entry].hasOwnProperty("disabled")) { if (entries[entry].disabled === true) { obj.textPrefix = `${obj.textPrefix}`; } } if (obj.textPrefix.includes("")) obj.textSuffix = `${obj.textSuffix}`; /* textFill += SingleEntryFill(section, entries[entry]); */ textFill += SingleEntryFill(obj); } /* end for (let entry in entries) */ } /* end switch (section) - central */ /* ############# Cumulate all section texts into one variable for final HTML filling. End section div tag ############### */ finalHTMLFill += `${textFill}\n

\n\n`; /* ################## End the Tab divs (must be after section ending div) ################# */ switch (section) { /* ending the tabs */ case "godmaster": // Main case "achievements": // Essentials case "huntersJournalOptional": // Journal case "items": // Collectibles case "geoRocks": // Geo Caches case "corniferNotes": // Secrets case "statistics": // Statistics case "hallOfGods": // Godmaster finalHTMLFill += `
`; break; } } /* end for (let section in sections) */ /* console.groupCollapsed("finalHTMLFill"); console.log(finalHTMLFill); console.groupEnd(); */ /* ################################## Horizontal line ############################################################################# */ finalHTMLFill += FLEUR_DIVIDE; /* --------------- Final single HTML access and fill here ------------------ */ document.getElementById("generated").innerHTML = finalHTMLFill; /* make tab switch buttons working (on click) - must run after inner HTML generation is finished */ document.querySelectorAll(".tab-switch").forEach((button) => { button.addEventListener("click", (e) => { PageSwitchTab(e.target.name); }); }); /* Check local storage first, and set the last selected Tab on the page (remembers last clicked tab) */ if (StorageAvailable('localStorage')) { if (localStorage.getItem("hkTabActive")) { PageSwitchTab(localStorage.getItem("hkTabActive")); } else { PageSwitchTab("main"); } } // finish benchmarking benchmarkTimes.GenerateInnerHTML.timeEnd = performance.now(); } function SectionDescription(section) { return `

${section.description}

`; } function SectionStart(section) { return `
\n`; } /** * Replaces the h2 titles with max percent values as read from the database */ function CompletionFillNoSave(section) { let id = ""; let h2 = ""; let h2id = ""; let percentBox = ""; // Percent Box let symbol = ""; let fullString = ""; id = section.id; h2 = section.h2; h2id = "h2-" + section.id; /* Display % only when showing Main Game Completion % sections */ switch (section.id) { case "hk-intro": case "hk-bosses": case "hk-charms": case "hk-equipment": case "hk-nailupgrades": case "hk-nailarts": case "hk-spells": case "hk-maskshards": case "hk-vesselfragments": case "hk-dreamnail": case "hk-warriordreams": case "hk-dreamers": case "hk-colosseum": case "hk-grimmtroupe": case "hk-lifeblood": case "hk-godmaster": symbol = "%"; break; default: symbol = ""; } percentBox = `
${(id === "hk-intro") ? 0: section.maxPercent}${symbol}
`; if (!section.hasOwnProperty("maxPercent")) percentBox = ""; fullString += `

${h2}${percentBox}

`; // ----------------- add True Completion h2 title ---------------- // switch (id) { case "hk-intro": fullString += `

True Completion
0.00%

`; break; default: } return fullString; } /** * Replaces the h2 titles with a current percent/max percent values as read from the database */ function CompletionFill(section) { let h2 = section.h2; let h2id = `

`; let cl = ""; let clGreen = "box-green"; let clRed = "box-red"; let cp = 0; // current Percent let midP = 0; // middle Percent let mp = 0; // max Percent let trueCompletionCurrent = 0; let trueCompletionTotal = 0; let trueCompletionPercent = 0; // True Completion % let symbol = ""; let percentBox = ""; let fullString = ""; (section.hasOwnProperty("percent")) ? cp = section.percent: cp = 0; (section.hasOwnProperty("midPercent")) ? midP = section.midPercent: midP = 0; // Don't use percent-box for Essentials, Achievements, Statistics etc. if (!section.hasOwnProperty("maxPercent")) { percentBox = ""; } // otherwise use percent-box with values cp/mp% else { mp = section.maxPercent; // Shards and Fragments correct calculations if (section.id === "hk-maskshards") { let perc = section.percent; (perc % 4) ? cp = Math.floor(perc / 4): cp = perc / 4; } else if (section.id === "hk-vesselfragments") { let perc = section.percent; (perc % 3) ? cp = Math.floor(perc / 3): cp = perc / 3; } // switches the box to red when a section (h2) is 0 if (cp === 0) { cl = ` ${clRed}`; } // switches the box to green when a section (h2) is completed else if (cp === mp) { cl = ` ${clGreen}`; } // default is blue (partially completed and starting value) else cl = ""; // Select which symbol or text to display (/ or something else depending on the section) switch (section.id) { // needed for Game Status to show percentage properly (adds a slash for all boxes except the Game Status one) case "hk-intro": break; // Hunter's Journal entries, Completed/Encountered of Total, e.g. 23/134 of 146 case "hk-journal": cp = `${cp}/${midP} of `; break; default: cp += "/"; } /* Display % only when showing Main Game Completion % sections */ switch (section.id) { case "hk-intro": // True Completion % reading and calculation for percent-box and box colors trueCompletionCurrent = section.extendedCompletionDone; trueCompletionTotal = section.extendedCompletionTotal; trueCompletionPercent = (trueCompletionCurrent / trueCompletionTotal) * 100; symbol = "%"; break; case "hk-bosses": case "hk-charms": case "hk-equipment": case "hk-nailupgrades": case "hk-nailarts": case "hk-spells": case "hk-maskshards": case "hk-vesselfragments": case "hk-dreamnail": case "hk-warriordreams": case "hk-dreamers": case "hk-colosseum": case "hk-grimmtroupe": case "hk-lifeblood": case "hk-godmaster": symbol = "%"; break; default: symbol = ""; } percentBox = `
${(section.id === "hk-intro") ? cp: `${cp}${section.maxPercent}`}${symbol}
`; } fullString += `\t${h2id}${h2}${percentBox}

\n`; // ----------------- add True Completion h2 title ---------------- // switch (section.id) { case "hk-intro": // switches the box to red when True Completion is 0 if (trueCompletionCurrent === 0) { cl = ` ${clRed}`; } // switches the box to green when True Completion is 100.00% else if (trueCompletionCurrent === trueCompletionTotal) { cl = ` ${clGreen}`; } // default is blue (partially completed and starting value) else cl = ""; fullString += `

True Completion
${trueCompletionPercent.toFixed(2)}${symbol}

`; break; default: } return fullString; } /* function SingleEntryFill(section, entry) { */ function SingleEntryFill(obj) { return [ obj.div, obj.icon, `${obj.b[0]}${obj.textPrefix}${obj.b[1]}`, obj.span[0], obj.p, obj.spoiler[0], `${obj.textSuffix}${obj.spoilerAfter}`, obj.spoiler[1], obj.span[1], "
\n" ].join(""); } /** * Adds HTML string to an element with a given ID. * @param {object} divId object containing div ID of the HTML element to append to * @param {string} content HTML contents to append */ function AppendHTML(divId, content) { document.getElementById(divId.id).innerHTML += "\n" + content; } function ToggleSaveModeSwitch() { let mode = this.value; let chooseFileButtonLabel = document.getElementById("file-input-label"); let analyzeTextButton = document.getElementById("save-area-read"); let saveTextArea = document.getElementById("save-area"); if (mode === "modeText") { this.value = "modeFile"; chooseFileButtonLabel.classList.remove("hidden"); analyzeTextButton.classList.add("hidden"); saveTextArea.classList.add("hidden"); /* alert(this.value); */ } else { this.value = "modeText"; chooseFileButtonLabel.classList.add("hidden"); analyzeTextButton.classList.remove("hidden"); saveTextArea.classList.remove("hidden"); /* Warning! focus() somehow makes the textarea text offset to the left */ /* saveTextArea.focus(); */ /* alert(this.value); */ } } /** * Toggles display of "hk-hints". On click with no parameters or on demand when called with a parameter * @param {string} param "hide", "show" or none (optional) */ function CheckboxHintsToggle(param = "none") { let checkboxId = document.getElementById("checkbox-hints"); switch (param) { case "hide": document.getElementById("hk-hints").classList.add("hidden"); checkboxId.value = "hints-off"; checkboxId.checked = false; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "unchecked"); } break; case "show": document.getElementById("hk-hints").classList.remove("hidden"); checkboxId.value = "hints-on"; checkboxId.checked = true; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "checked"); } break; default: // This runs when the checkbox is not checked if (checkboxId.checked === false) { document.getElementById("hk-hints").classList.add("hidden"); checkboxId.value = "hints-off"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "unchecked"); } } // This runs when the checkbox is checked else { document.getElementById("hk-hints").classList.remove("hidden"); checkboxId.value = "hints-on"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxHints", "checked"); } } } } /** * Toggles display of ".spoiler-span" class. On click with no parameters or on demand when called with a parameter * @param {string} param "hide", "show" or none (optional) */ function CheckboxSpoilersToggle(param = "none") { let checkboxId = document.getElementById("checkbox-spoilers"); let allClassElements = document.querySelectorAll(".spoiler-span"); let allClassElementsRed = document.querySelectorAll(".spoiler-red"); let length = allClassElements.length; let lengthRed = allClassElementsRed.length; switch (param) { case "hide": for (let i = 0; i < length; i++) { allClassElements[i].classList.add("blurred"); } for (let i = 0; i < lengthRed; i++) { allClassElementsRed[i].classList.add("blurred"); } checkboxId.value = "spoilers-off"; checkboxId.checked = false; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "unchecked"); } break; case "show": for (let i = 0; i < length; i++) { allClassElements[i].classList.remove("blurred"); } for (let i = 0; i < lengthRed; i++) { allClassElementsRed[i].classList.remove("blurred"); } checkboxId.value = "spoilers-on"; checkboxId.checked = true; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "checked"); } break; default: // This runs when the checkbox is not checked if (checkboxId.checked === false) { for (let i = 0; i < length; i++) { allClassElements[i].classList.add("blurred"); } for (let i = 0; i < lengthRed; i++) { allClassElementsRed[i].classList.add("blurred"); } checkboxId.value = "spoilers-off"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "unchecked"); } break; } // This runs when the checkbox is checked else { for (let i = 0; i < length; i++) { allClassElements[i].classList.remove("blurred"); } for (let i = 0; i < lengthRed; i++) { allClassElementsRed[i].classList.remove("blurred"); } checkboxId.value = "spoilers-on"; // remember this choice for subsequent page visits and browser restarts if (StorageAvailable('localStorage')) { localStorage.setItem("hkCheckboxSpoilers", "checked"); } } } } /** * Hides all other tabs, except the one which button was clicked (shows only the chosen tab) * @param {String} clickedButton The click target (button clicked) */ function PageSwitchTab(clickedButton) { let sectionList = document.querySelectorAll(".large-section"); let buttonList = document.querySelectorAll(".tab-switch"); /* Make Active Tab Visible */ for (let i = 0, length = sectionList.length; i < length; i++) { /* Other tabs except the clicked one */ if (sectionList[i].id !== `tab-${clickedButton}`) { if (!sectionList[i].classList.contains("hidden")) { sectionList[i].classList.add("hidden"); } } /* The clicked tab */ else { if (sectionList[i].classList.contains("hidden")) { sectionList[i].classList.remove("hidden"); } } } /* Make Active Button stand out */ for (let i = 0, length = buttonList.length; i < length; i++) { /* Other buttons except the clicked one */ if (buttonList[i].id !== `button-switch-${clickedButton}`) { if (buttonList[i].classList.contains("tab-active")) { buttonList[i].classList.remove("tab-active"); } } /* The clicked button */ else { if (!buttonList[i].classList.contains("tab-active")) { buttonList[i].classList.add("tab-active"); } } } /* remember this choice for subsequent page visits and browser restarts */ if (StorageAvailable('localStorage')) { if (clickedButton) { localStorage.setItem("hkTabActive", clickedButton); } } } /** * Detects whether Storage is both supported and available. * MDN WebDocs https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage * @param {Storage} type type of storage. Ex. "localStorage" or "sessionStorage" * @returns {Boolean} */ function StorageAvailable(type) { var storage; try { storage = window[type]; var x = '__storage_test__'; storage.setItem(x, x); storage.removeItem(x); return true; } catch (e) { return e instanceof DOMException && ( // everything except Firefox e.code === 22 || // Firefox e.code === 1014 || // test name field too, because code might not be present // everything except Firefox e.name === 'QuotaExceededError' || // Firefox e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && // acknowledge QuotaExceededError only if there's something already stored (storage && storage.length !== 0); } } /** * Fills the innerHTML of a given HTML Element with provided contents * @param {string} elementId Element ID to update * @param {string} textFill Updated contents (innerHTML) */ function FillInnerHTML(elementId, textFill) { const element = document.getElementById(elementId); element.innerHTML = textFill; } /** * Focuses, selects and copies to clipboard contents inside a clicked element. Includes optional tooltip update after the copying is done. * @param {MouseEvent} mouseEvent from the clicked element (AddEventListener) * @param {string} tooltipId Element ID of the tooltip to update * @param {string} tooltipFill Updated contents of the tooltip */ function SelectCopyInputText(mouseEvent, tooltipId = "", tooltipFill = "") { const element = document.getElementById(mouseEvent.target.id); // this prevents the un-selected effect after clicking the second time (clears all selection first) if (window.getSelection) { window.getSelection().removeAllRanges(); } element.focus(); // best to focus the element first before selecting element.select(); element.setSelectionRange(0, 99999); // for mobile devices // Copy the text inside the text field to clipboard document.execCommand("copy"); // optional tooltip showing if (tooltipFill.length && tooltipId.length) FillInnerHTML(tooltipId, tooltipFill); } function FileNameFormat(file, nameLength, beginLength, endLength) { var fileName = file.name; /* Shorten the file name if too long */ if (fileName.length > nameLength) { let begin = fileName.slice(0, beginLength); // take X characters from the beginning (0) let end = fileName.slice(-endLength); // take X characters from the end (-) fileName = `${begin}..${end}`; } return fileName; } function FileDateFormat(file) { var fileDate = new Date(file.lastModified); var year = fileDate.getFullYear(); var month = fileDate.getMonth() + 1; var day = fileDate.getDate(); var hour = fileDate.getHours(); var minutes = fileDate.getMinutes(); /* var seconds = fileDate.getSeconds(); */ if (month < 10) month = "0" + month; if (day < 10) day = "0" + day; if (hour < 10) hour = "0" + hour; if (minutes < 10) minutes = "0" + minutes; /* if (seconds < 10) seconds = "0" + seconds; */ return `${year}.${month}.${day} ${hour}:${minutes}`; } /* ========================== Event Listeners ========================== */ /* --------------- Toggle visibility of scroll arrow ------------------ */ document.addEventListener("scroll", () => { TogglePageScrollElement( /* the document element root () */ ROOT, /* Which element to toggle visibility */ SCROLL_BUTTON, /* How far the user has to scroll to show the element */ 0.1); }); /* ---------------- Scroll to top when clicked ------------------- */ SCROLL_BUTTON.addEventListener("click", () => { ScrollToElement(ROOT); }); /* ------------- Auto select & copy to clipboard when the save file location input text is clicked once ------------- */ document.getElementById("save-location-input").addEventListener("click", (e) => { let tooltip = document.getElementById("save-location-input-tooltip"); SelectCopyInputText(e, "save-location-input-tooltip", "Copied save files location to clipboard"); /* make sure that the tooltip is centered */ tooltip.style.marginLeft = `-${tooltip.offsetWidth / 2}px`; }, false); /* -------------- Switch text back to the default on mouse out -------------- */ document.getElementById("save-location-input").addEventListener("mouseout", () => { let tooltip = document.getElementById("save-location-input-tooltip"); /* change the text and center only when the text was different */ if (tooltip.innerHTML !== "Click once to copy to clipboard") { FillInnerHTML("save-location-input-tooltip", "Click once to copy to clipboard"); /* make sure that the tooltip is centered */ tooltip.style.marginLeft = `-${tooltip.offsetWidth / 2}px`; } }, false); /* ------------ Toggle Save Mode Switch: Text Mode or File Mode -------------- */ document.getElementById("toggle-mode").addEventListener("click", ToggleSaveModeSwitch, false); /* ------------- Checkbox functions ---------------------- */ document.getElementById("checkbox-hints").addEventListener("click", CheckboxHintsToggle, false); document.getElementById("checkbox-spoilers").addEventListener("click", CheckboxSpoilersToggle, false); /* ------------ Drag & drop file to the window -------------- */ window.addEventListener('dragover', (event) => { event.stopPropagation(); event.preventDefault(); // Style the drag-and-drop as a "copy file" operation. event.dataTransfer.dropEffect = 'copy'; }); window.addEventListener('drop', (event) => { event.stopPropagation(); event.preventDefault(); const dt = event.dataTransfer; /* Launch save file analyzing */ LoadSaveFile(dt, performance.now()); var label = document.getElementById("save-area-file").nextElementSibling; var labelInitialText = label.innerHTML; /* Shorten the file name if longer than 16 characters. Display first 10 characters and last 4. */ var fileName = FileNameFormat(dt.files[0], 16, 10, 4); /* Display a custom formatted last modified date. */ var fileDate = FileDateFormat(dt.files[0]); /* Display the save file name and date on the button */ if (fileName) { label.innerHTML = `${SYMBOL_FILE}${fileName}
${fileDate}
`; } else { label.innerHTML = labelInitialText; } }); /* ---------- Monitor file input change and show the file name when file is loaded ----------- */ document.getElementById("save-area-file").addEventListener("change", (event) => { var label = document.getElementById("save-area-file").nextElementSibling; var labelInitialText = label.innerHTML; /* Shorten the file name if longer than 16 characters. Display first 10 characters and last 4. */ var fileName = FileNameFormat(event.target.files[0], 16, 10, 4); /* Display a custom formatted last modified date. */ var fileDate = FileDateFormat(event.target.files[0]); if (fileName) { label.innerHTML = `${SYMBOL_FILE}${fileName}
${fileDate}
`; } else { label.innerHTML = labelInitialText; } }); /* -------- Clean the text area and file input from leftover save file if present (Firefox especially) -------- */ document.addEventListener("DOMContentLoaded", () => { (async () => { document.getElementById("save-area").value = ""; document.getElementById("save-area-file").value = ""; })(); }); /* ------------------------- Exports ------------------------------- */ export { GenerateInnerHTML, AppendHTML, CheckboxHintsToggle, CheckboxSpoilersToggle, StorageAvailable, Benchmark, benchmarkTimes };