many improvements to questions
This commit is contained in:
@@ -42,6 +42,53 @@
|
|||||||
hx-target=".offcanvas-body"
|
hx-target=".offcanvas-body"
|
||||||
>Preset questions</button>
|
>Preset questions</button>
|
||||||
|
|
||||||
|
<details><summary>
|
||||||
|
Question Block Title
|
||||||
|
</summary>
|
||||||
|
<p>Title of the question block.</p>
|
||||||
|
<input id="question_block_title_input" type="text" name="title" value="{{case_detail.question_schema.title}}" class="form-control" placeholder="Question title" aria-label="Question title">
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
<details open><summary>Add Question</summary>
|
||||||
|
<details><summary>Modify Questions</summary>
|
||||||
|
<div id="questions-container" style="margin-top: 20px;">
|
||||||
|
<!-- Questions will be dynamically rendered here -->
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<div>
|
||||||
|
<label for="question-type-select">Select Question Type:</label>
|
||||||
|
<select id="question-type-select" class="form-select">
|
||||||
|
<option value="radio">Multiple Choice (Radio)</option>
|
||||||
|
<option value="dropdown">Multiple Choice (Dropdown)</option>
|
||||||
|
<option value="text">Text Input</option>
|
||||||
|
<option value="multiselect">Multiselect</option>
|
||||||
|
<option value="yesno">Yes/No</option>
|
||||||
|
<option value="truefalse">True/False</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="options-configurator" style="margin-top: 10px; ">
|
||||||
|
<label>Enter Options:</label>
|
||||||
|
<div id="question-options-container">
|
||||||
|
<!-- Individual option input fields will be added here -->
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" id="add-option-button" style="margin-top: 10px;">Add Option</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<label for="question-title-input">Question Title:</label>
|
||||||
|
<input id="question-title-input" type="text" class="form-control" placeholder="Enter question title">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<label for="question-description-input">Question Description:</label>
|
||||||
|
<input id="question-description-input" type="text" class="form-control" placeholder="Enter question description">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-sm" id="add-new-question">Add New Question</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" id="reset-question-options">Reset</button>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel"
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel"
|
||||||
>
|
>
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
@@ -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 = `
|
||||||
|
<input type="text" class="form-control option-field" placeholder="Option ${optionIndex}" style="display: inline-block; width: 90%;">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm remove-option-button" style="display: inline-block; width: 8%;">×</button>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<strong>${question.title || key}</strong>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm delete-question-button" data-question-key="${key}" style="margin-left: 10px;">Delete</button>
|
||||||
|
`;
|
||||||
|
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();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user