diff --git a/deck-new.js b/deck-new.js index 273f364..2531e46 100644 --- a/deck-new.js +++ b/deck-new.js @@ -8,7 +8,11 @@ window.bUsingPinyinConversion = false; window.writer = null; -// Convert each word in a sentence to pinyin +/** + * Convert each word in a sentence to pinyin + * @param {string} string - The input non-pinyin string + * @returns {string} - The output pinyin-ified string + */ function pinyinify(string) { // Ugly af but it's the only way we can do it ig const pinyin = @@ -63,6 +67,14 @@ function pinyinify(string) { return arr.join(" "); } +/** + * Constructs a preview card HTML element for a phrase or card + * @param { number } index - The index into "it" + * @param { Object? } it - The struct that whose data will be used to construct the preview card + * @param { HTMLElement } owner - Parent HTML element + * @returns { Object } - Reference to the current modified object. Could be "it", a card from local storage(if "it" is + * a phrase reference, or it's null) or a new card. + */ function constructPreviewCardGeneric(index, it, owner) { let root = addElement("div", "", `character-preview-${index}`, "card centered", "", owner); @@ -129,6 +141,18 @@ function constructPreviewCardGeneric(index, it, owner) return lit; } +/** + * Constructs an input element + * @param {HTMLElement} container - The parent HTML element to attach to + * @param { string } id - ID for the input element + * @param { string } classT - Class for the input element + * @param { string } type - Type attribute of the input element + * @param { string } ariaLabel - Aria label attribute of the input element + * @param { string } name - Name attribute of the input element + * @param { string } previewID - ID of the text field in the preview card that corresponds to the owning edit card + * @param { function } callback - Callback function when the data is changed + * @returns { HTMLElement } - The resulting input element + */ function constructInputElement(container, id, classT, type, ariaLabel, name, previewID, callback) { let input = addElement("input", "", id, classT, "", container); @@ -171,11 +195,27 @@ function constructPhraseEditCardPreview(it) { return lit; } +/** + * Checks if a given character variant exists + * @param { string } character - The character in question + * @param { string } postfix - Variant postfix, like "-jp" or "-ko" for Japanese and Korean respectively + * @returns {Promise} - JSON data about the given character + */ async function testVariantExists(character, postfix) { return await charDataLoader(character + postfix, null, null); } +/** + * Constructs the character variant select box + * @param { HTMLElement } container - The parent HTML element + * @param { string } id - ID for the select box + * @param { string } classT - Class for the select box + * @param { string } ariaLabel - Aria label attribute for the select box + * @param { string } name - Name attribute for the select box + * @param { Object } it - Card object this corresponds to. Used to change the character's value in the change callback + * @returns {Promise} + */ async function constructCharacterVariantSelect(container, id, classT, ariaLabel, name, it) { let select = addElement("select", "", id, classT, "", container); @@ -194,6 +234,14 @@ async function constructCharacterVariantSelect(container, id, classT, ariaLabel, }); } +/** + * Reconstructs the definition list for a given card or phrase + * @param { HTMLElement } previewList - The HTML element that contains the definitions list inside its corresponding preview card + * @param { HTMLElement } editList - The HTML element that contains the definitions list inside its corresponding edit card + * @param { string[] } definitions - The list of definition strings + * @param { boolean } bReadOnly - Whether the definition list is read only, i.e. when editing a phrase and a character + * from it has a corresponding card that is already in the deck + */ function reconstructDefinitionList(previewList, editList, definitions, bReadOnly) { if (previewList === null || editList === null) @@ -224,6 +272,13 @@ function reconstructDefinitionList(previewList, editList, definitions, bReadOnly } } +/** + * Constructs an edit card + * @param { string|number } index - Index into the array that holds "it" + * @param { Object } it - Object whose data will be used to construct an edit card + * @param { HTMLElement } root - The root element, to which the edit card should be attached to + * @param { boolean } bPhrase - Whether you're editing a phrase + */ function constructEditCard(index, it, root, bPhrase) { let lit = it; @@ -321,7 +376,7 @@ function constructEditCard(index, it, root, bPhrase) for (let i in lit.phrase) { constructPreviewCardGeneric(i, lit, phrasePreviewContainer); - constructEditCard(i, lit, cardEditSection); + constructEditCard(i, lit, cardEditSection, false); } }); } @@ -379,7 +434,7 @@ function constructListElements() if (it["character"]) { - constructPreviewCardGeneric(0, it, cardEditSection, false); + constructPreviewCardGeneric(0, it, cardEditSection); constructEditCard(0, it, cardEditSection, true); for (let i = 0; i < cardEditSection.childNodes.length; i++) cardEditSection.insertBefore(cardEditSection.childNodes[i], cardEditSection.firstChild); @@ -391,7 +446,7 @@ function constructListElements() constructEditCard("phrase", it, cardEditSection, true); for (let i in it["phrase"]) { - constructPreviewCardGeneric(i, it, phrasePreviewSectionContainer, true) + constructPreviewCardGeneric(i, it, phrasePreviewSectionContainer) constructEditCard(i, it, cardEditSection, false); } } @@ -405,8 +460,8 @@ function constructListElements() } else { - let lit = constructPreviewCardGeneric(0, null, cardEditSection, false); - constructEditCard("0", lit, cardEditSection, false); + let lit = constructPreviewCardGeneric(0, null, cardEditSection); + constructEditCard(0, lit, cardEditSection, false); // Reverse children of $("card-edit-section") because we display the preview before the edit widget for (let i = 0; i < cardEditSection.childNodes.length; i++) diff --git a/deck.js b/deck.js index 88c8b26..246ffb6 100644 --- a/deck.js +++ b/deck.js @@ -4,11 +4,18 @@ window.HOUR_UNIX = 36000000; window.MINUTE_UNIX = 60000; window.SECOND_UNIX = 1000; +/** + * The export button callback. Stringifies the current data and exports it. + */ function updateExportButton() { - const dt = window.localStorageData.cards; + const dt = window.localStorageData; + const data = { + cards: dt.cards, + phrases: dt.phrases + } - let file = new Blob([JSON.stringify(dt)], { type: "application/json;charset=utf-8" }); + let file = new Blob([JSON.stringify(data)], { type: "application/json;charset=utf-8" }); const link = document.createElement("a"); link.href = URL.createObjectURL(file); @@ -17,14 +24,21 @@ function updateExportButton() URL.revokeObjectURL(link.href); } +/** + * The import deck button callback. Imports a deck from a file. + * @param {string} f - The element in question + */ function importDeck(f) { let bExecuted = confirm(lc.import_deck_confirm_text); if (bExecuted) { const reader = new FileReader(); - reader.addEventListener("load", function(){ + reader.addEventListener("load", (e) => { let dt = window.localStorageData; - dt.cards.push.apply(dt.cards, JSON.parse(this.result.toString())); + let data = JSON.parse(e.target.result.toString()); + + dt.cards.push.apply(dt.cards, data.cards); + dt.phrases.push.apply(dt.phrases, data.phrases); saveToLocalStorage(dt); document.location.reload(); @@ -45,6 +59,9 @@ function clearDeck() { } } +/** + * Sets data about the current user. Deals with calculating most of the statistics and showcasing them + */ function setProfileCardData() { $("total-sessions-field").textContent += window.localStorageData.sessions; @@ -107,10 +124,13 @@ function setProfileCardData() averageKnowledge.textContent = `${lc.average_knowledge_level}: ${knowledge.toFixed(2)}/${window.MAX_KNOWLEDGE_LEVEL}`; } -// it - struct -// index - used to create UUIDs. For normal cards, its offset by the number of phrases -// constainer - container element -// localIndex - non-unique index +/** + * Constructs a card HTML element + * @param { Object } it - Struct for the card data + * @param { int } index - Used to create UUIDs. For normal cards, it's offset by the number of phrases + * @param { HTMLElement } container - HTML element to attach to + * @param { int } localIndex - non-unique index + */ function constructCard(it, index, container, localIndex) { // Add parent div @@ -189,6 +209,9 @@ function constructCard(it, index, container, localIndex) } } +/** + * Main function for the deck page + */ function deckmain() { setProfileCardData(); diff --git a/index.js b/index.js index b87cb3f..459c1dd 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,9 @@ // ------------------- CONSTANT BLOCK EDIT IF RUNNING ON A CUSTOM SYSTEM ------------------ window.MAX_KNOWLEDGE_LEVEL = 4; window.MAX_POINTS_ON_CHARACTER = 0.25; -window.ADD_POINTS_ON_ERROR_3_4 = 0.1875; // 3/4 of 0.25 -window.ADD_POINTS_ON_ERROR_1_2 = 0.125; // 1/2 or 2/4 of 0.25 -window.ADD_POINTS_ON_ERROR_1_3 = 0.0625; // 1/3 of 0.25 +window.ADD_POINTS_ON_ERROR_3_4 = 0.1875; // 3/4 of 0.25 +window.ADD_POINTS_ON_ERROR_1_2 = 0.125; // 1/2 or 2/4 of 0.25 +window.ADD_POINTS_ON_ERROR_1_3 = 0.0625; // 1/3 of 0.25 window.CARD_WRITER_SIZE = 100; window.CARD_WRITER_STROKE_ANIMATION_SPEED = 1.25; @@ -14,7 +14,7 @@ window.CARD_DEFAULT_PREVIEW_NAME = "Preview Name" window.WRITER_PADDING = 5; window.WRITER_RADICAL_COLOUR = "#c87e74"; -window.WRITER_SLEEP_AFTER_COMPLETE = 1200; // In ms +window.WRITER_SLEEP_AFTER_COMPLETE = 1200; // In ms window.WRITER_SHOW_HINT_ON_ERRORS = 3; window.WRITER_SHOW_HINT_ON_ERRORS_LVL_3 = 1; @@ -22,12 +22,23 @@ window.WRITER_SHOW_HINT_ON_ERRORS_LVL_3 = 1; window.localStorageData = null; -// Troll jQuery developers +/** + * Troll jQuery developers. Returns the element with the given id + * @param {string} x - ID of the element + * @returns {HTMLElement} - The element in question + */ function $(x) { return document.getElementById(x); } +/** + * Given an element, an event and a function to execute, tracks the given event and executes the provided callback + * function when the animation or transition on the given element has finished playing + * @param { HTMLElement } element - Element on which to track the event + * @param { string } event - Event type, like "click" + * @param { function } f - Function to run after animation + */ function runEventAfterAnimation(element, event, f) { element.bWaitForAnimation = false; @@ -47,6 +58,11 @@ function runEventAfterAnimation(element, event, f) element.addEventListener("transitionend", func); } +/** + * Adds a text node to an element + * @param { HTMLElement } container - Parent element of the text node + * @param { string } text - The text that the node contains + */ function addTextNode(container, text) { container.appendChild(document.createTextNode(text)); @@ -58,18 +74,31 @@ async function charDataLoader(character, _, __) let response = await fetch(`https://cdn.jsdelivr.net/gh/MadLadSquad/hanzi-writer-data-youyin/data/${character}.json`) if (response.status !== 200) { - console.error(`Bad response from the character database, this is mainly caused by missing characters. Response code: ${response.status}`); + console.warn(`Bad response from the character database, this is mainly caused by missing characters. Response code: ${response.status}`); return; } return await response.json(); } -function saveToLocalStorage(string) +/** + * Saves an object to the "youyinCardData" entry in the browser's local storage + * @param { Object } obj - The object in question + */ +function saveToLocalStorage(obj) { - window.localStorage.setItem("youyinCardData", JSON.stringify(string)); + window.localStorage.setItem("youyinCardData", JSON.stringify(obj)); } -// Returns an element, creates an element with the given parameters and appends it +/** + * Utility function to create an HTML element in a single line + * @param { string } elType - Type of the new element + * @param { string } content - Text content of the new element + * @param { string } id - ID of the new element + * @param { string } classType - Class of the new element + * @param { string } data - Data that will be stored as the value of the "arbitrary-data" attribute + * @param { HTMLElement } parentEl - Element to become the parent of the new element + * @returns { HTMLElement } - The element that was created + */ function addElement(elType, content, id, classType, data, parentEl) { let el = document.createElement(elType); @@ -113,6 +142,7 @@ function fisherYates(array) } } +// Some legacy users may be lacking variants as part of their character card objects, so this function fixes this function fixLegacyCharacterVariants() { for (let i in window.localStorageData.cards) @@ -126,6 +156,12 @@ function fixLegacyCharacterVariants() } } +/** + * Redirects the user when selecting a new language from the language select box + * @param { HTMLElement } selectWidget - The select box widget + * @param { string } localStorageLang - Current language + * @param { string|null } previous - Previous language + */ function redirectWithLanguage(selectWidget, localStorageLang, previous) { let url = location.href.split("/"); diff --git a/main-page.js b/main-page.js index b699ebc..c13a4c9 100644 --- a/main-page.js +++ b/main-page.js @@ -20,7 +20,7 @@ window.bMobile = false; window.linkChildren = null; -// This function uses some dark magic that works half the time in order to calculate the size of the mainpage viewport +// This function uses some dark magic that works half the time in order to calculate the size of the main page viewport // and main elements. Here are some issues: // TODO: On portrait screens if the resolution changes this sometimes breaks and a refresh is needed, would be good if it was fixed. // Probably check out main.css and the main-page media query @@ -43,13 +43,9 @@ function getDrawElementHeight() let finalHeight = window.innerHeight - unusedSpace + listWidget.getBoundingClientRect().height; window.bMobile = navigator.userAgent.toLowerCase().includes("mobile"); if (window.bMobile) - { finalHeight -= (footer.getBoundingClientRect().height); - } else - { finalHeight -= (listWidget.getBoundingClientRect().height + footer.getBoundingClientRect().height); - } if (mainEl.getBoundingClientRect().width < finalHeight) finalHeight = mainEl.getBoundingClientRect().width - (getComputedStyle(startButtonWriterSection).paddingLeft.replace("px", "") * 2); @@ -93,6 +89,14 @@ function writerOnCorrectStroke(_) window.backwardsErrors = 0; } +/** + * Generic function that updates the sidebar element for a phrase or card + * @param { string } prefix - Prefix, this is different depending on whether you're editing the phrase or character card + * data + * @param { string } spelling - Spelling of the given character or phrase + * @param { string } errors - The number of errors for the given character or phrase. String because it may be localised + * @param { Object|null } obj - Object currently editing. May be null if not editing an object + */ function updateIndividualSidebarElementText(prefix, spelling, errors, obj) { $(`${prefix}-info-widget-spelling`).textContent = spelling; @@ -101,14 +105,17 @@ function updateIndividualSidebarElementText(prefix, spelling, errors, obj) list.replaceChildren(); if (obj !== null) - { for (let i in obj.definitions) - { addElement("li", obj.definitions[i], "", "", "", list); - } - } } +/** + * Changes the sidebar text + * @param { Object|null } phrase - Phrase to edit. May be null if only editing a character card + * @param { number } phraseNum - Number of phrases in the deck. Used to show which phrase you're currently on + * @param { Object|null } card - Card to edit. May be null if a phrase doesn't contain the card but contains the character + * @param { number } cardNum - Number of cards. Used to show which card you're currently on + */ function changeSidebarText(phrase, phraseNum, card, cardNum) { let definitionParagraph = $("character-info-widget-def-p"); @@ -129,7 +136,7 @@ function changeSidebarText(phrase, phraseNum, card, cardNum) function resetSidebar() { - // Ugly ahh code to reset to the inital state + // Ugly ahh code to reset to the initial state $("character-info-widget-spelling").textContent = `${lc.phrases_count_spelling}: ${lc.to_be_loaded}`; $("character-info-widget-errors").textContent = `${lc.phrases_count_cards}: 0/0; ${lc.phrases_count_errors}: 0`; @@ -164,6 +171,13 @@ function setWriterState(ref) } } +/** + * Computes how to score a phrase or character card + * @param { number } strokes - Strokes in the given character + * @param { number } errors - Errors committed for the given character + * @param { number } knowledge - Knowledge for the given phrase or character card + * @returns { number } - The final score + */ function computeScore(strokes, errors, knowledge) { let pointsPerStroke = (window.MAX_POINTS_ON_CHARACTER / strokes); diff --git a/marketplace.js b/marketplace.js index a890c42..06ae3fc 100644 --- a/marketplace.js +++ b/marketplace.js @@ -5,12 +5,22 @@ async function loadMarketplaceData(file) let response = await fetch(`https://cdn.jsdelivr.net/gh/MadLadSquad/YouyinPublicDeckRepository@latest/${file}`) if (response.status !== 200) { - console.error(`Bad response from the character database, this is mainly caused by missing characters. Response code: ${response.status}`); + console.warn(`Bad response from the character database, this is mainly caused by missing characters. Response code: ${response.status}`); return; } return await response.json(); } +/** + * Constructs a marketplace element + * @param { number } val - Index in the deck directory + * @param { HTMLElement } deckContainer - Container HTML element for the card + * @param { Object } it - JSON object for the element + * @param { string } type1 - Deck type + * @param { string } type2 - Deck type as a UI string + * @param { string } folder - Folder in which the marketplace element is in. Empty if it's not in a folder + * @returns { Promise } + */ async function constructElement(val, deckContainer, it, type1, type2, folder) { // Get data for the given marketplace entry and process it @@ -50,7 +60,7 @@ async function constructElement(val, deckContainer, it, type1, type2, folder) }); // Stupid ahhhh whitespace adding code because web dev is stupid - div.appendChild(document.createTextNode("\u00A0")); + addTextNode(div, "\u00A0"); runEventAfterAnimation(addElement("button", lc.deck_source, `source-button-${type1}-${val}`, "card-button-edit", filename, div), "click", async function(e) { @@ -74,11 +84,22 @@ async function constructElement(val, deckContainer, it, type1, type2, folder) }); } +/** + * Creates an error text element + * @param { HTMLElement } deckContainer - Container HTML element + * @param { Object } response - JSON response object + * @param { string } marketplaceType - Marketplace type, official or community + */ function createErrorElement(deckContainer, response, marketplaceType) { addElement("h1", `Error ${response.status}: Couldn't load the ${marketplaceType} marketplace, retry later!`, "", "error-text centered, vcentered", "", deckContainer); } +/** + * Constructs elements for official decks + * @param { HTMLElement } deckContainer - Container HTML element + * @returns {Promise} + */ async function handleOfficialRepos(deckContainer) { let response = await fetch("https://api.github.com/repos/MadLadSquad/YouyinPublicDeckRepository/contents/"); @@ -96,10 +117,14 @@ async function handleOfficialRepos(deckContainer) } } -// Creates cards for the community decks +/** + * Constructs elements for community decks + * @param { HTMLElement } deckContainer - Container HTML element + * @returns {Promise} + */ async function handleCommunityRepos(deckContainer) { - // Start from community, we will then iterate trough all the release folders + // Start from community, we will then iterate through all the release folders let response = await fetch("https://api.github.com/repos/MadLadSquad/YouyinPublicDeckRepository/contents/community"); if (response.status !== 200) {