diff --git a/atlas/templates/atlas/collection_case_detail.html b/atlas/templates/atlas/collection_case_detail.html index 146eb488..003f900b 100644 --- a/atlas/templates/atlas/collection_case_detail.html +++ b/atlas/templates/atlas/collection_case_detail.html @@ -42,6 +42,53 @@ hx-target=".offcanvas-body" >Preset questions +
+ Question Block Title + +

Title of the question block.

+ +
+ + +
Add Question +
Modify Questions +
+ +
+
+
+ + +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+ + + +
+
@@ -97,6 +144,343 @@ }); })) }); + + + $(document).ready(function() { + const question_editor = window.jsoneditor_id_question_schema; + const questionBlockTitleInput = document.getElementById('question_block_title_input'); + // Wrap the existing onChange handler + const originalOnChange = question_editor.$$.ctx[question_editor.$$.props.onChange]; + const addNewQuestionButton = document.getElementById('add-new-question'); + const optionsConfigurator = document.getElementById('options-configurator'); + const questionOptionsContainer = document.getElementById('question-options-container'); + const addOptionButton = document.getElementById('add-option-button'); + const questionTypeSelect = document.getElementById('question-type-select'); + const questionTitleInput = document.getElementById('question-title-input'); + const questionDescriptionInput = document.getElementById('question-description-input'); + const resetQuestionOptionsButton = document.getElementById('reset-question-options'); + const questionsContainer = document.getElementById('questions-container'); + + question_editor.updateProps({ + onChange: (updatedContent, previousContent, context) => { + // Call the original onChange handler if it exists + if (typeof originalOnChange === 'function') { + originalOnChange(updatedContent, previousContent, context); + } + + const value = updatedContent.json ? updatedContent.json.title : ''; + if (value !== questionBlockTitleInput.value) { + questionBlockTitleInput.value = value; // Update the input field + } + } + }); + + // Sync changes from the input field to the editor + // Yes this is a bit backwards but it works + questionBlockTitleInput.addEventListener('input', () => { + try { + currentContent = JSON.parse(question_editor.get().text); + } + catch { + currentContent = JSON.parse(JSON.stringify(question_editor.get().json)); + } + if (currentContent) { + currentContent.title = questionBlockTitleInput.value; // Update the title in the editor + question_editor.update({json: currentContent}); // Apply the changes to the editor + } + }); + + + + // Show or hide the options configurator based on the selected question type + questionTypeSelect.addEventListener('change', () => { + const selectedType = questionTypeSelect.value; + if (selectedType === 'radio' || selectedType === 'dropdown' || selectedType === 'multiselect') { + optionsConfigurator.style.display = 'block'; + } else { + optionsConfigurator.style.display = 'none'; + } + }); + + // Add a new option input field + addOptionButton.addEventListener('click', () => { + const optionIndex = questionOptionsContainer.children.length + 1; + const optionInput = document.createElement('div'); + optionInput.className = 'option-input'; + optionInput.style.marginBottom = '5px'; + optionInput.innerHTML = ` + + + `; + questionOptionsContainer.appendChild(optionInput); + + // Add event listener to remove the option + optionInput.querySelector('.remove-option-button').addEventListener('click', () => { + questionOptionsContainer.removeChild(optionInput); + }); + }); + + // Reset the question options + resetQuestionOptionsButton.addEventListener('click', () => { + // Clear the title and description fields + questionTitleInput.value = ''; + questionDescriptionInput.value = ''; + + // Clear all option input fields + while (questionOptionsContainer.firstChild) { + questionOptionsContainer.removeChild(questionOptionsContainer.firstChild); + } + + // Reset the question type to the default value + questionTypeSelect.value = 'radio'; + + // Hide the options configurator if necessary + optionsConfigurator.style.display = 'block'; + + toastr.success('Question options have been reset.'); + }); + + // Add a new question + addNewQuestionButton.addEventListener('click', () => { + try { + currentContent = JSON.parse(question_editor.get().text); + } catch { + currentContent = JSON.parse(JSON.stringify(question_editor.get().json)); + } + + if (!currentContent.properties) { + currentContent.properties = {}; + current_question_number = 1; + } else { + current_question_number = Object.keys(currentContent.properties).length + 1; + } + + // Get the selected question type + const selectedType = questionTypeSelect.value; + + // Collect options from individual input fields + const options = Array.from(questionOptionsContainer.querySelectorAll('.option-field')) + .map(input => input.value.trim()) + .filter(option => option !== ""); // Exclude empty options + + // Get the title and description + const title = questionTitleInput.value || `Question ${current_question_number}`; + const description = questionDescriptionInput.value || "Enter the question description here."; + + // Define the new question template based on the selected type + let newQuestion; + if (selectedType === 'radio') { + newQuestion = { + enum: options.length > 0 ? options : ["Option 1", "Option 2", "Option 3"], // Default options if none provided + type: "string", + title: title, + format: "radio", + description: description + }; + } else if (selectedType === 'text') { + newQuestion = { + type: "string", + title: title, + format: "text", + description: description + }; + } else if (selectedType === 'dropdown') { + newQuestion = { + enum: options.length > 0 ? options : ["Option 1", "Option 2", "Option 3"], // Default options if none provided + type: "string", + title: title, + format: "select", + description: description + }; + } else if (selectedType === 'multiselect') { + newQuestion = { + type: "array", + items: { + enum: options.length > 0 ? options : ["Option 1", "Option 2", "Option 3"], // Default options if none provided + type: "string" + }, + title: title, + format: "select2", // Multiselect format + description: description, + uniqueItems: true // Ensure unique selections + }; + } else if (selectedType === 'yesno') { + newQuestion = { + enum: ["Yes", "No"], + type: "string", + title: title, + format: "radio", + description: description + }; + } else if (selectedType === 'truefalse') { + newQuestion = { + enum: ["True", "False"], + type: "string", + title: title, + format: "radio", + description: description + }; + } + + newQuestion.propertyOrder = current_question_number; // Set the property order + // Add the new question to the properties + currentContent.properties[`question_${current_question_number}`] = newQuestion; + + // Update the editor with the new question + question_editor.update({ json: currentContent }); + toastr.success(`New question added: ${title}`); + renderQuestions(); // Re-render the questions list + }); + + // Function to render the list of questions with delete and drag-and-drop functionality + function renderQuestions() { + questionsContainer.innerHTML = ''; // Clear the container + + try { + let currentContent; + try { + currentContent = JSON.parse(question_editor.get().text); + } catch { + currentContent = JSON.parse(JSON.stringify(question_editor.get().json)); + } + const properties = currentContent.properties || {}; + + // Sort questions by propertyOrder + const sortedKeys = Object.keys(properties).sort((a, b) => { + const orderA = properties[a].propertyOrder !== undefined ? properties[a].propertyOrder : 1000; + const orderB = properties[b].propertyOrder !== undefined ? properties[b].propertyOrder : 1000; + return orderA - orderB; + }); + + sortedKeys.forEach((key, index) => { + const question = properties[key]; + const questionElement = document.createElement('div'); + questionElement.className = 'question-item'; + questionElement.style.marginBottom = '10px'; + questionElement.setAttribute('draggable', 'true'); // Make the element draggable + questionElement.setAttribute('data-question-key', key); + questionElement.setAttribute('data-index', index); + + questionElement.innerHTML = ` + ${question.title || key} + + `; + questionsContainer.appendChild(questionElement); + + // Add event listener to delete the question + questionElement.querySelector('.delete-question-button').addEventListener('click', (e) => { + const questionKey = e.target.getAttribute('data-question-key'); + deleteQuestion(questionKey); + }); + + // Add drag-and-drop event listeners + questionElement.addEventListener('dragstart', handleDragStart); + questionElement.addEventListener('dragover', handleDragOver); + questionElement.addEventListener('drop', handleDrop); + questionElement.addEventListener('dragend', handleDragEnd); + }); + } catch (error) { + console.error('Error rendering questions:', error); + } +} + + // Drag-and-drop event handlers + let draggedElement = null; + + function handleDragStart(e) { + draggedElement = this; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/html', this.outerHTML); + this.classList.add('dragging'); + } + + function handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + } + + +function handleDrop(e) { + e.preventDefault(); + if (draggedElement !== this) { + const draggedIndex = parseInt(draggedElement.getAttribute('data-index')); + const targetIndex = parseInt(this.getAttribute('data-index')); + + try { + let currentContent; + try { + currentContent = JSON.parse(question_editor.get().text); + } catch { + currentContent = JSON.parse(JSON.stringify(question_editor.get().json)); + } + const properties = currentContent.properties || {}; + const keys = Object.keys(properties); + + // Sort keys by propertyOrder + const sortedKeys = keys.sort((a, b) => { + const orderA = properties[a].propertyOrder !== undefined ? properties[a].propertyOrder : Infinity; + const orderB = properties[b].propertyOrder !== undefined ? properties[b].propertyOrder : Infinity; + return orderA - orderB; + }); + + // Move the dragged question to the target position + const draggedKey = sortedKeys.splice(draggedIndex, 1)[0]; // Remove dragged key + sortedKeys.splice(targetIndex, 0, draggedKey); // Insert dragged key at target position + + // Reassign propertyOrder to ensure uniqueness and correct order + sortedKeys.forEach((key, index) => { + properties[key].propertyOrder = index + 1; + }); + + // Assign propertyOrder to any questions without it + keys.forEach((key) => { + if (properties[key].propertyOrder === undefined) { + properties[key].propertyOrder = sortedKeys.length + 1; + sortedKeys.push(key); + } + }); + + currentContent.properties = properties; + question_editor.update({ json: currentContent }); + toastr.success('Questions reordered successfully.'); + renderQuestions(); // Re-render the questions list + } catch (error) { + console.error('Error reordering questions:', error); + toastr.error('Failed to reorder questions.'); + } + } +} + + function handleDragEnd() { + this.classList.remove('dragging'); + } + + // Function to delete a question + function deleteQuestion(questionKey) { + try { + try { + currentContent = JSON.parse(question_editor.get().text); + } catch { + currentContent = JSON.parse(JSON.stringify(question_editor.get().json)); + } + if (currentContent.properties && currentContent.properties[questionKey]) { + delete currentContent.properties[questionKey]; // Remove the question + question_editor.update({ json: currentContent }); // Update the editor + toastr.success(`Question "${questionKey}" has been deleted.`); + renderQuestions(); // Re-render the questions list + } + } catch (error) { + console.error('Error deleting question:', error); + toastr.error('Failed to delete the question.'); + } + } + // Initial render of questions + renderQuestions(); + + }); + + + {% endblock %} \ No newline at end of file