diff --git a/templates/js/annotationOverlay.js b/templates/js/annotationOverlay.js index c20ec15..c65937e 100644 --- a/templates/js/annotationOverlay.js +++ b/templates/js/annotationOverlay.js @@ -1,3 +1,9 @@ +/** + * Ensures the annotation stage (overlay container) exists inside the given container, + * creating it if necessary. The stage holds both the SVG line layer and the label div. + * @param {HTMLElement} container - The parent element to attach the stage to. + * @returns {HTMLElement} The existing or newly created annotation stage element. + */ function ensureStage(container) { let stage = container.querySelector(".annotation-stage"); if (!stage) { @@ -12,6 +18,11 @@ function ensureStage(container) { return stage; } +/** + * Removes the annotation stage and all its contents from the given container. + * @param {HTMLElement} container - The container whose annotation stage should be removed. + * @returns {void} + */ export function clearAnnotations(container) { if (!container) return; const stage = container.querySelector(".annotation-stage"); @@ -66,8 +77,14 @@ function normalizedPointToPx(pt, box, norm) { // <--- RENAMED to reflect change } /** - * Draw labels + lines from a JSON object: - * { annotations: [...], normalized_geometry: { normX, normY, normW, normH } } + * Draws text annotation labels and pointer lines onto the given container. + * Uses normalized geometry from the annotation payload to map the label and + * pointer coordinates onto the displayed pixel size of the container. + * @param {HTMLElement} container - The element to draw annotations into. + * @param {Object} annotationsJson - Text annotation payload from the API. + * @param {Array} annotationsJson.annotations - Array of text annotation objects. + * @param {Object} annotationsJson.normalized_geometry - Normalized geometry for the slide crop. + * @returns {void} */ export function drawAnnotations(container, annotationsJson) { if (!container || !annotationsJson) return; @@ -95,7 +112,7 @@ export function drawAnnotations(container, annotationsJson) { }; // Get the list of annotations. - const list = annotationsJson.annotations || annotationsJson.text_annotations || []; + const list = annotationsJson.annotations || []; list.forEach((a) => { if (!a || !a.text_box) return; @@ -130,7 +147,7 @@ export function drawAnnotations(container, annotationsJson) { if (validateEvent.detail.isValid) { el.classList.add("valid-link"); - + el.addEventListener("click", () => { const clickEvent = new CustomEvent("annotationSelected", { detail: { text: a.text_content, annotationData: a }, @@ -166,7 +183,12 @@ export function drawAnnotations(container, annotationsJson) { stage.__lastJson = annotationsJson; } -/** Re-draw on container resize using the last JSON used. */ +/** + * Attaches a ResizeObserver to the container that redraws annotations whenever + * the container's dimensions change. Does nothing if an observer is already attached. + * @param {HTMLElement} container - The container to watch for size changes. + * @returns {void} + */ export function attachAutoscale(container) { const stage = ensureStage(container); if (stage.__resizeObs) return; // already attached diff --git a/templates/js/api.js b/templates/js/api.js index 876a6f6..778a25d 100644 --- a/templates/js/api.js +++ b/templates/js/api.js @@ -11,6 +11,12 @@ const API_CONFIG = { } }; +/** + * Fetches the combined boneset/bone/subbone data from the backend API. + * This is the primary data source used to populate all dropdowns on page load. + * @returns {Promise} Combined data object containing `bonesets`, `bones`, and `subbones` arrays. + * @throws {Error} If the network request fails or the server returns a non-OK response. + */ export async function fetchCombinedData() { const API_URL = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.COMBINED_DATA}`; diff --git a/templates/js/coloredRegionsOverlay.js b/templates/js/coloredRegionsOverlay.js index c701654..d558c9b 100644 --- a/templates/js/coloredRegionsOverlay.js +++ b/templates/js/coloredRegionsOverlay.js @@ -491,6 +491,7 @@ export async function displayColoredRegions(imageElement, boneId, imageIndex = 0 /** * Clear all colored region overlays from a container * @param {HTMLElement} container - The container element + * @returns {void} */ export function clearColoredRegions(container) { if (!container) return; @@ -501,6 +502,7 @@ export function clearColoredRegions(container) { /** * Clear all colored region overlays in the entire image container + * @returns {void} */ export function clearAllColoredRegions() { const container = document.getElementById("bone-image-container"); diff --git a/templates/js/description.js b/templates/js/description.js index 6a5b16e..b2f1dc5 100644 --- a/templates/js/description.js +++ b/templates/js/description.js @@ -1,5 +1,13 @@ import { fetchDescription } from "./api.js"; +/** + * Fetches the description HTML for the given bone/subbone ID and + * places it inside the `#description-Container` element. + * Shows an error message in the container if the fetch fails. + * @param {string} id - The bone or subbone ID (e.g. `"ilium"`, `"iliac_crest"`), + * used to construct the filename `{id}_description.json`. + * @returns {Promise} + */ export async function loadDescription(id) { const container = document.getElementById("description-Container"); container.innerHTML = ""; diff --git a/templates/js/dropdowns.js b/templates/js/dropdowns.js index 5307776..498a033 100644 --- a/templates/js/dropdowns.js +++ b/templates/js/dropdowns.js @@ -8,17 +8,27 @@ document.addEventListener("DOMContentLoaded", () => { showPlaceholder(); }); +/** + * Returns the `#bone-image-container` element, which serves as the host for + * displayed bone images and their annotation overlays. + * @returns {HTMLElement|null} The image container element, or null if not found. + */ function getImageStage() { return (document.getElementById("bone-image-container")); } -/** Helper: fetch images for a bone/sub-bone and render them */ + +/** Helper: fetch images for a bone/sub-bone and render them. + * @param {string} boneId - The bone or subbone ID to load images for. + * @param {Object} [options={}] - Options forwarded to `displayBoneImages`. + * @returns {Promise} + */ async function loadBoneImages(boneId, options = {}) { const stage = getImageStage(); if (stage) { - clearAnnotations(stage); - stage.classList.remove("with-annotations"); + clearAnnotations(stage); + stage.classList.remove("with-annotations"); } if (!boneId) { @@ -43,6 +53,12 @@ async function loadBoneImages(boneId, options = {}) { } } +/** + * Populates the boneset `` elements. + * Each listener loads images, descriptions, and annotations appropriate to the selection. + * @param {Object} combinedData - The full application data set. + * @param {Array} combinedData.bonesets - Array of boneset objects. + * @param {Array} combinedData.bones - Array of bone objects. + * @param {Array} combinedData.subbones - Array of subbone objects. + * @returns {void} + */ export function setupDropdownListeners(combinedData) { const bonesetSelect = document.getElementById("boneset-select"); const boneSelect = document.getElementById("bone-select"); diff --git a/templates/js/imageCaptions.js b/templates/js/imageCaptions.js index ca3bf7c..edb1824 100644 --- a/templates/js/imageCaptions.js +++ b/templates/js/imageCaptions.js @@ -1,5 +1,13 @@ // Image captions for bone images, extracted from PowerPoint slides +/** + * A lookup map of image captions keyed by bone/subbone ID. + * Each entry provides caption strings for up to two images (`image1`, `image2`). + * Captions describe the anatomical view shown in the corresponding image + * (e.g. lateral aspect, medial aspect). + * + * @type {Object.} + */ export const imageCaptions = { // Bony Pelvis (main boneset) "bony_pelvis": { diff --git a/templates/js/imageDisplay.js b/templates/js/imageDisplay.js index ffa4cd6..50e39db 100644 --- a/templates/js/imageDisplay.js +++ b/templates/js/imageDisplay.js @@ -5,13 +5,20 @@ import { fetchAnnotations } from "./api.js"; let currentBoneId = null; +/** + * Returns the `#bone-image-container` DOM element. + * @returns {HTMLElement|null} The image container element, or null if not found. + */ function getImageContainer() { return ( document.getElementById("bone-image-container") ); } -/** Helper function to get captions for a boneId */ +/** Helper function to get captions for a boneId + * @param {string|null} boneId - The bone or subbone ID. + * @returns {{image1: string|null, image2: string|null}} Caption strings for the two images, or nulls if not found. + */ function getCaptionsForBone(boneId) { if (!boneId || !imageCaptions[boneId]) { return { image1: null, image2: null }; @@ -19,7 +26,9 @@ function getCaptionsForBone(boneId) { return imageCaptions[boneId]; } -/** Helper to clear existing caption container */ +/** Removes the `#caption-container` element from the DOM if it exists. + * @returns {void} + */ function clearCaptionContainer() { const existingCaptions = document.getElementById("caption-container"); if (existingCaptions) { @@ -27,7 +36,11 @@ function clearCaptionContainer() { } } -/** Empty-state / clearing */ +/** + * Renders the empty-state placeholder message inside the image container + * and clears all text annotations, colored regions, and captions. + * @returns {void} + */ export function showPlaceholder() { const c = getImageContainer(); if (!c) return; @@ -48,6 +61,10 @@ export function showPlaceholder() { if (imagesContent) imagesContent.classList.remove("has-images"); } +/** + * Clears all images, text annotations, colored regions, and captions from the image container. + * @returns {void} + */ export function clearImages() { const c = getImageContainer(); if (c) { @@ -65,8 +82,15 @@ export function clearImages() { if (imagesContent) imagesContent.classList.remove("has-images"); } -/** ---- Public entry: render images array -------------------------------- - * Optionally pass { annotationsUrl: 'templates/data/annotations/xyz.json', boneId: 'bone_name' } +/** + * Renders one or more bone images into the image container, applying the appropriate + * layout (single, two-up, or grid) based on the number of images provided. + * Also loads colored region overlays and text annotation overlays if applicable. + * @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of image objects to + * display. + * @param {Object} [options={}] - Optional display configuration. + * @param {string} [options.boneId] - Bone ID used for colored region overlays. + * @returns {void} */ export function displayBoneImages(images, options = {}) { const container = getImageContainer(); @@ -109,7 +133,12 @@ export function displayBoneImages(images, options = {}) { } } -/* Single image */ +/** + * Renders a single bone image with its colored region overlay and text annotations. + * @param {{url?: string, src?: string, alt?: string, filename?: string}} image - The image object to display. + * @param {HTMLElement} container - The image container element. + * @returns {void} + */ function displaySingleImage(image, container) { const captions = getCaptionsForBone(currentBoneId); @@ -176,6 +205,14 @@ function displaySingleImage(image, container) { } } +/** + * Renders two bone images side by side, each with its own colored region overlay. + * Appends a two-column caption bar beneath the images if captions are available. + * @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of exactly two image + * objects. + * @param {HTMLElement} container - The image container element. + * @returns {void} + */ function displayTwoImages(images, container) { const captions = getCaptionsForBone(currentBoneId); @@ -264,7 +301,13 @@ function displayTwoImages(images, container) { } } -/** 3+ images grid */ +/** + * Renders three or more bone images in a wrapping grid layout. + * Does not load colored regions or annotations (used for supplementary views). + * @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of image objects. + * @param {HTMLElement} container - The image container element. + * @returns {void} + */ function displayMultipleImages(images, container) { const wrapper = document.createElement("div"); wrapper.className = "multiple-image-wrapper"; diff --git a/templates/js/main.js b/templates/js/main.js index 133d848..9403eb5 100644 --- a/templates/js/main.js +++ b/templates/js/main.js @@ -9,12 +9,6 @@ import quizManager from "./quiz.js"; let combinedData = { bonesets: [], bones: [], subbones: [] }; -/** - * Handles bone selection from dropdown - * @param {string} boneId - The ID of the selected bone - */ -// handleBoneSelection is defined inside DOMContentLoaded after DOM elements are known - document.addEventListener("DOMContentLoaded", async () => { initializeSearch(); await initializeSidebar(); @@ -90,6 +84,13 @@ document.addEventListener("DOMContentLoaded", async () => { clearViewer(); }); +/** + * Populates the subbone `` element to keep in sync. + * @param {function(string): void} updateDescription - Callback invoked with the selected subbone ID + * whenever the navigation changes. + * @returns {void} + */ export function setupNavigation(prevButton, nextButton, subboneDropdown, updateDescription) { // Setup Previous/Next button navigation prevButton.addEventListener("click", () => { @@ -59,24 +68,46 @@ function resetToInitialState() { window.location.reload(); } +/** + * Sets the currently active bone and its associated subbones for navigation, + * resetting the index to the first subbone. + * @param {string} bone - The ID of the currently selected bone. + * @param {string[]} boneSubbones - Array of subbone IDs belonging to that bone. + * @returns {void} + */ export function setBoneAndSubbones(bone, boneSubbones) { currentBone = bone; subbones = boneSubbones || []; currentSubboneIndex = subbones.length > 0 ? 0 : -1; } +/** + * Decrements the current subbone index (moves to the previous subbone), if greater than 0. + * @returns {void} + */ function prevSubbone() { if (currentSubboneIndex > 0) { currentSubboneIndex--; } } +/** + * Increments the current subbone index (moves to the next subbone), if less than the array of subbones. + * @returns {void} + */ function nextSubbone() { if (currentSubboneIndex < subbones.length - 1) { currentSubboneIndex++; } } +/** + * Syncs the subbone dropdown to the current index and invokes the description callback. + * Does nothing if no subbones are loaded. + * @param {HTMLSelectElement} subboneDropdown - The subbone select element to update. + * @param {function(string): void} updateDescription - Callback invoked with the selected subbone ID. + * @returns {void} + */ function updateUI(subboneDropdown, updateDescription) { if (subbones.length === 0 || currentSubboneIndex === -1) return; @@ -85,6 +116,13 @@ function updateUI(subboneDropdown, updateDescription) { updateDescription(selectedSubbone); } +/** + * Enables or disables the previous/next buttons depending on whether any subbones + * are currently loaded. + * @param {HTMLButtonElement} prevButton - The "previous" navigation button. + * @param {HTMLButtonElement} nextButton - The "next" navigation button. + * @returns {void} + */ export function disableButtons(prevButton, nextButton) { const disabled = subbones.length === 0; prevButton.disabled = disabled; diff --git a/templates/js/quiz.js b/templates/js/quiz.js index 5b86ecd..492db69 100644 --- a/templates/js/quiz.js +++ b/templates/js/quiz.js @@ -1,6 +1,12 @@ import {fetchBoneData} from "./api.js"; import {displayColoredRegions} from "./coloredRegionsOverlay.js"; +/** + * Manages the interactive bone-identification quiz. + * Fetches bones and subbones from the API, generates randomised questions, + * handles answer scoring, and controls quiz UI visibility. + * @class + */ class QuizManager { constructor() { this.questions = []; @@ -14,7 +20,13 @@ class QuizManager { } /** - * Initialize the quiz system + * Loads bone data from the API, builds the master question pool, and + * attaches UI event listeners. Must be called before starting a quiz. + * @param {Object} data - Bone and bone part data. + * @param {Object[]} [data.bones] - Bone objects. + * @param {Object[]} [data.subbones] - Bone part objects. + * @returns {Promise} Resolves to `true` if initialisation succeeded + * and fails if there was an error or too few items to form a quiz. */ async initialize(data) { this.allBones = data.bones || []; @@ -31,7 +43,9 @@ class QuizManager { } /** - * Create a master pool of all bones and subbones + * Populates {@link QuizManager#masterQuestionPool} by combining all bones + * and subbones fetched during initialisation. + * @returns {void} */ createMasterQuestionPool() { this.masterQuestionPool = []; @@ -58,7 +72,9 @@ class QuizManager { } /** - * Setup event listeners for quiz UI + * Attaches click handlers to the start-quiz, exit-quiz, and next-question + * buttons in the DOM. + * @returns {void} */ setupEventListeners() { const quizButton = document.getElementById("start-quiz-btn"); @@ -79,7 +95,10 @@ class QuizManager { } /** - * Generate quiz questions using the master pool + * Randomly selects items from the master pool to build + * {@link QuizManager#questions}, each with one correct answer and three + * distractors. + * @returns {void} */ generateQuestions() { this.questions = []; @@ -117,7 +136,11 @@ class QuizManager { } /** - * Generate wrong answer choices from the master pool + * Generates the wrong answer choices for a quiz question by selecting + * names from the master pool, excluding the correct item. + * @param {string} correctItemId - The ID of the correct answer item to exclude. + * @param {number} count - Number of wrong answer choices to generate. + * @returns {string[]} Array of wrong answer choice strings. */ generateWrongAnswers(correctItemId, count) { const wrongAnswers = []; @@ -136,7 +159,11 @@ class QuizManager { } /** - * Shuffle array using Fisher-Yates algorithm + * Returns a new array with the same elements in a random order using the + * Fisher-Yates algorithm. + * @template T + * @param {T[]} array - The array to shuffle. + * @returns {T[]} A new shuffled array (the original is not mutated). */ shuffleArray(array) { const shuffled = [...array]; @@ -148,137 +175,144 @@ class QuizManager { } /** - * Fetch and display bone image from API - */ -async fetchBoneImage(itemId, container) { - try { - const data = await fetchBoneData(itemId); - - console.debug(`Bone data for ${itemId}:`, data); - - // Check if image exists in the response - if (data.images && data.images.length > 0) { - // Create image element with error handling - let imageUrl = data.images[0].url; - const img = document.createElement("img"); - img.src = imageUrl; - img.alt = "Bone image for quiz question"; - img.style.maxWidth = "100%"; - img.style.maxHeight = "400px"; - img.style.objectFit = "contain"; - img.style.borderRadius = "8px"; - - img.onerror = () => { - console.error(`Failed to load image from: ${imageUrl}`); + * Fetches bone data from the API and renders the primary image into + * `container`. Falls back to a placeholder on missing or failed images. + * Also attempts to overlay coloured regions once the image loads. + * @param {string} itemId - The bone or subbone ID whose image to display. + * @param {HTMLElement} container - The DOM element to render the image into. + * @returns {Promise} + */ + async fetchBoneImage(itemId, container) { + try { + const data = await fetchBoneData(itemId); + + console.debug(`Bone data for ${itemId}:`, data); + + // Check if image exists in the response + if (data.images && data.images.length > 0) { + // Create image element with error handling + let imageUrl = data.images[0].url; + const img = document.createElement("img"); + img.src = imageUrl; + img.alt = "Bone image for quiz question"; + img.style.maxWidth = "100%"; + img.style.maxHeight = "400px"; + img.style.objectFit = "contain"; + img.style.borderRadius = "8px"; + + img.onerror = () => { + console.error(`Failed to load image from: ${imageUrl}`); + container.innerHTML = ` +
+

🦴

+

Image failed to load

+

${itemId}

+
+ `; + }; + + img.onload = () => { + displayColoredRegions(img, itemId, 0).catch(err => { + console.warn(`Could not display colored regions for ${itemId}:`, err); + }); + console.log(`Image loaded successfully for ${itemId}`); + }; + + container.innerHTML = ""; + container.appendChild(img); + } else { + console.warn(`No images found for ${itemId}`); + // No image available - show placeholder container.innerHTML = `

🦴

-

Image failed to load

+

Image not available

${itemId}

`; - }; - - img.onload = () => { - displayColoredRegions(img, itemId, 0).catch(err => { - console.warn(`Could not display colored regions for ${itemId}:`, err); - }); - console.log(`Image loaded successfully for ${itemId}`); - }; - - container.innerHTML = ""; - container.appendChild(img); - } else { - console.warn(`No images found for ${itemId}`); - // No image available - show placeholder + } + } catch (error) { + console.error(`Error fetching image for ${itemId}:`, error); + // Show error placeholder container.innerHTML = `

🦴

-

Image not available

-

${itemId}

+

Unable to load image

+

${error.message}

`; } - } catch (error) { - console.error(`Error fetching image for ${itemId}:`, error); - // Show error placeholder - container.innerHTML = ` -
-

🦴

-

Unable to load image

-

${error.message}

-
- `; } -} /** - * Start the quiz + * Generates a fresh set of questions, resets state, switches the UI to + * quiz mode, and displays the first question. + * @returns {void} */ - /** - * Start the quiz - */ -startQuiz() { - // Generate questions - this.generateQuestions(); + startQuiz() { + // Generate questions + this.generateQuestions(); - if (this.questions.length === 0) { - alert("Unable to generate quiz questions. Please try again."); - return; - } + if (this.questions.length === 0) { + alert("Unable to generate quiz questions. Please try again."); + return; + } - // Reset quiz state - this.currentQuestionIndex = 0; - this.score = 0; - this.answered = false; + // Reset quiz state + this.currentQuestionIndex = 0; + this.score = 0; + this.answered = false; - // Show quiz container, hide main content - this.showQuizMode(); + // Show quiz container, hide main content + this.showQuizMode(); // Restore the quiz structure (in case we're coming from results screen) - const quizContainer = document.getElementById("quiz-container"); - if (quizContainer) { - quizContainer.innerHTML = ` -
-
- Question 1 of ${this.questions.length} - Score: 0/${this.questions.length} + const quizContainer = document.getElementById("quiz-container"); + if (quizContainer) { + quizContainer.innerHTML = ` +
+
+ Question 1 of ${this.questions.length} + Score: 0/${this.questions.length} +
+
- -
- -
-

What bone or bone part is this?

-
-
-
- - - -
- -
- `; + +
+

What bone or bone part is this?

+
+
+
+ + + +
+ +
+ `; - // Re-attach exit button listener - const exitBtn = document.getElementById("exit-quiz-btn"); - if (exitBtn) { - exitBtn.onclick = () => this.exitQuiz(); - } + // Re-attach exit button listener + const exitBtn = document.getElementById("exit-quiz-btn"); + if (exitBtn) { + exitBtn.onclick = () => this.exitQuiz(); + } - // Re-attach next button listener - const nextBtn = document.getElementById("next-question-btn"); - if (nextBtn) { - nextBtn.onclick = () => this.nextQuestion(); + // Re-attach next button listener + const nextBtn = document.getElementById("next-question-btn"); + if (nextBtn) { + nextBtn.onclick = () => this.nextQuestion(); + } } - } - // Display first question - this.displayQuestion(); -} + // Display first question + this.displayQuestion(); + } /** - * Display current question + * Renders the current question (image, answer choices, progress) in the + * quiz UI. Calls {@link QuizManager#showResults} when all questions are + * exhausted. + * @returns {void} */ displayQuestion() { if (this.currentQuestionIndex >= this.questions.length) { @@ -337,7 +371,11 @@ startQuiz() { } /** - * Display answer choice buttons + * Renders the answer choice buttons for the given question. + * @param {Object} question - The current question object. + * @param {string[]} question.allAnswers - All answer choices to display. + * @param {string} question.correctAnswer - The correct answer text. + * @returns {void} */ displayAnswerChoices(question) { const choicesContainer = document.getElementById("quiz-choices"); @@ -356,7 +394,12 @@ startQuiz() { } /** - * Handle answer selection + * Processes a user's answer selection: updates the score, highlights + * correct/incorrect buttons, shows feedback, and reveals the next-question + * button. + * @param {string} selectedAnswer - The answer text the user clicked. + * @param {string} correctAnswer - The correct answer text for this question. + * @returns {void} */ handleAnswerClick(selectedAnswer, correctAnswer) { if (this.answered) return; // Prevent multiple answers @@ -399,7 +442,11 @@ startQuiz() { } /** - * Show feedback after answer + * Displays a correct/incorrect feedback banner below the answer choices. + * @param {boolean} isCorrect - Whether the selected answer was correct. + * @param {string} correctAnswer - The correct answer text, shown on + * incorrect responses. + * @returns {void} */ showFeedback(isCorrect, correctAnswer) { const feedback = document.getElementById("quiz-feedback"); @@ -423,7 +470,8 @@ startQuiz() { } /** - * Move to next question + * Advances to the next question in the sequence. + * @returns {void} */ nextQuestion() { this.currentQuestionIndex++; @@ -432,12 +480,11 @@ startQuiz() { /** - * Show quiz results + * Replaces the quiz container with a results summary showing the final + * score and a contextual message. Attaches retry and exit handlers. + * @returns {void} */ -/** - * Show quiz results - */ -showResults() { + showResults() { const percentage = Math.round((this.score / this.questions.length) * 100); let message = ""; @@ -487,14 +534,14 @@ showResults() { console.debug("Retry button found:", retryBtn); console.debug("Exit button found:", exitBtn); - + if (retryBtn) { retryBtn.onclick = () => { console.debug("TRY AGAIN CLICKED!"); this.startQuiz(); }; } - + if (exitBtn) { exitBtn.onclick = () => { console.log("EXIT CLICKED!"); // Debug @@ -505,7 +552,8 @@ showResults() { } /** - * Show quiz mode (hide main content) + * Hides the main page content and shows the quiz modal overlay. + * @returns {void} */ showQuizMode() { const mainContent = document.querySelector(".container"); @@ -516,7 +564,8 @@ showResults() { } /** - * Exit quiz and return to main view + * Ends the active quiz, restores the main view, and resets all quiz state. + * @returns {void} */ exitQuiz() { this.isQuizActive = false; diff --git a/templates/js/search.js b/templates/js/search.js index 9a2d59f..ae6eaa3 100644 --- a/templates/js/search.js +++ b/templates/js/search.js @@ -4,7 +4,11 @@ let selectedIndex = -1; let searchTimeout; let isInitialized = false; -// Handle search result clicks and keyboard navigation +/** + * Sets up the search bar's input, keyboard navigation, and outside-click + * handlers. Should be called once after the DOM is ready. + * @returns {void} + */ export function initializeSearch() { if (isInitialized) return; const searchBar = document.getElementById("search-bar"); @@ -69,6 +73,12 @@ export function initializeSearch() { }); } +/** + * Sends a search query to the API and renders the resulting HTML into + * the search-results container. + * @param {string} query - The search string typed by the user. + * @returns {Promise} + */ async function performSearch(query) { const searchResultsContainer = document.getElementById("search-results"); const searchLoading = document.getElementById("search-loading"); @@ -89,6 +99,11 @@ async function performSearch(query) { } } +/** + * Attaches click-event listeners to every `.search-result` element currently + * in the DOM so that clicking one triggers {@link selectSearchResult}. + * @returns {void} + */ function attachClickHandlers() { const results = document.querySelectorAll(".search-result"); results.forEach(result => { @@ -99,6 +114,12 @@ function attachClickHandlers() { }); } +/** + * Highlights the result at `selectedIndex` and removes highlighting from all + * others, scrolling the highlighted item into view. + * @param {NodeListOf} results - The current set of result elements. + * @returns {void} + */ function updateSelection(results) { results.forEach((result, index) => { if (index === selectedIndex) { @@ -110,6 +131,14 @@ function updateSelection(results) { }); } +/** + * Reads the data attributes from a result element and delegates to + * {@link updateDropdowns} to navigate the main viewer to that bone. + * @param {HTMLElement} resultElement - The clicked or keyboard-selected result + * element carrying `data-type`, `data-boneset`, `data-bone`, and optionally + * `data-subbone` attributes. + * @returns {void} + */ function selectSearchResult(resultElement) { const type = resultElement.dataset.type; const bonesetId = resultElement.dataset.boneset; @@ -125,6 +154,16 @@ function selectSearchResult(resultElement) { clearSearch(); } +/** + * Cascades selection through the boneset → bone → subbone dropdowns by + * dispatching `change` events with short timeouts between each tier. + * @param {string} type - The type of result: `'boneset'`, `'bone'`, or `'subbone'`. + * @param {string} bonesetId - The boneset ID to set on the boneset dropdown. + * @param {string} boneId - The bone ID to set on the bone dropdown (when relevant). + * @param {string} subboneId - The subbone ID to set on the subbone dropdown + * (only used when `type` is `'subbone'`). + * @returns {void} + */ function updateDropdowns(type, bonesetId, boneId, subboneId) { const bonesetSelect = document.getElementById("boneset-select"); const boneSelect = document.getElementById("bone-select"); @@ -159,12 +198,21 @@ function updateDropdowns(type, bonesetId, boneId, subboneId) { } } +/** + * Clears the search bar input and removes all results. + * @returns {void} + */ function clearSearch() { const searchBar = document.getElementById("search-bar"); searchBar.value = ""; clearSearchResults(); } +/** + * Empties the search-results container, hides the loading indicator, and + * resets the keyboard-navigation index. + * @returns {void} + */ function clearSearchResults() { const searchResults = document.getElementById("search-results"); const searchLoading = document.getElementById("search-loading"); diff --git a/templates/js/sidebar.js b/templates/js/sidebar.js index 21caa68..c809ba5 100644 --- a/templates/js/sidebar.js +++ b/templates/js/sidebar.js @@ -1,3 +1,8 @@ +/** + * Wires up the sidebar toggle button to lazily load `sidebar.html` and + * slide the sidebar panel in and out. + * @returns {Promise} + */ export async function initializeSidebar() { const toggleButton = document.getElementById("toggle-sidebar"); const sidebarContainer = document.getElementById("sidebar-container"); diff --git a/templates/js/viewer.js b/templates/js/viewer.js index ba9ef81..9367490 100644 --- a/templates/js/viewer.js +++ b/templates/js/viewer.js @@ -33,7 +33,8 @@ export function displayAnnotations(annotations) { /** * Main function to display complete bone data (annotations only - images handled by imageDisplay.js) - * @param {Object} boneData - The complete bone object + * @param {Object} boneData - The complete bone object. + * @returns {void} */ export function displayBoneData(boneData) { if (!boneData) { @@ -46,6 +47,7 @@ export function displayBoneData(boneData) { /** * Clears the viewer display (annotations only - images handled by imageDisplay.js) + * @returns {void} */ export function clearViewer() { // Images are cleared by imageDisplay.js