further question editing improvements
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
{% if previous %}
|
||||
@@ -14,23 +14,31 @@
|
||||
<a href="{% url 'atlas:collection_case_details' collection.id next.id %}">Next question</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2>Case: <a href="{% url 'atlas:case_detail' case_detail.case.pk %}">{{case_detail.case.title}}</a></h2>
|
||||
|
||||
{% if case_detail.question_schema is not None and not case_detail.question_answers %}
|
||||
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">Answers not defined!</h4>
|
||||
<p>A question schema has been defined but no question answers are found. Please set them below.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">Answers not defined!</h4>
|
||||
<p>A question schema has been defined but no question answers are found. Please set them below.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>This form allows you to add questions to a case. A example of how the quesiton will be displayed is shown below.</p>
|
||||
<p>If answers are supplied the question will be automarked</p>
|
||||
<details class="help-text">
|
||||
<summary><i class="bi bi-info-circle"></i> Help</summary>
|
||||
<p>This form allows you to add questions to a case. A example of how the quesiton will be displayed is shown below.</p>
|
||||
<p>If answers are supplied the question will be automarked</p>
|
||||
|
||||
<p>The question system is built using https://github.com/json-editor/json-editor, this allows for highly flexible forms / question design at a cost of some complexity. To help with this a number of preset questions are avalible to choose from (these can then be edited if needed). It is also possible to copy questions from other cases in the collection. </p>
|
||||
<p>The question system is built using https://github.com/json-editor/json-editor, this allows for highly flexible forms / question design at a cost of some complexity. To help with this a number of preset questions are avalible to choose from (these can then be edited if needed). It is also possible to copy questions from other cases in the collection. </p>
|
||||
|
||||
<p>In most cases it is not necessary to edit the JSON schema directly as questions can be added / edited via the GUI.</p>
|
||||
|
||||
<p>To save the question schema you need to click the "Save Questions" button at the bottom of the page. The page will then refresh and you can see how questions will be displayed at the bottom of the page.</p>
|
||||
|
||||
</details>
|
||||
|
||||
Import schema from:
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight"
|
||||
@@ -43,49 +51,54 @@
|
||||
>Preset questions</button>
|
||||
|
||||
<details><summary>
|
||||
Question Block Title
|
||||
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;">
|
||||
<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><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>
|
||||
</details>
|
||||
<details open id="add-question-block"><summary>Add/Edit Question</summary>
|
||||
<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="dropdown-select">Multiple Choice (Dropdown - Select2)</option>
|
||||
<option value="text">Text Input</option>
|
||||
<option value="textarea">Text Area</option>
|
||||
<option value="multiselect">Multiselect</option>
|
||||
<option value="yesno">Yes/No</option>
|
||||
<option value="truefalse">True/False</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="range">Range</option>
|
||||
|
||||
<div id="options-configurator" style="margin-top: 10px; ">
|
||||
<label>Enter Options:</label>
|
||||
<div id="question-options-container">
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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"
|
||||
>
|
||||
@@ -101,7 +114,7 @@
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit" value="save" name="submit">Submit</button>
|
||||
<button type="submit" value="save" name="submit">Save Questions</button>
|
||||
</form>
|
||||
|
||||
{% comment %} <h2>Blank form</h2>
|
||||
@@ -150,14 +163,14 @@
|
||||
// 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');
|
||||
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) => {
|
||||
@@ -175,423 +188,502 @@
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
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';
|
||||
}
|
||||
});
|
||||
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 = `
|
||||
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);
|
||||
questionOptionsContainer.appendChild(optionInput);
|
||||
|
||||
// Add event listener to remove the option
|
||||
optionInput.querySelector('.remove-option-button').addEventListener('click', () => {
|
||||
questionOptionsContainer.removeChild(optionInput);
|
||||
});
|
||||
});
|
||||
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 = '';
|
||||
// Reset the question options
|
||||
resetQuestionOptionsButton.onclick = resetQuestion;
|
||||
|
||||
// Clear all option input fields
|
||||
while (questionOptionsContainer.firstChild) {
|
||||
questionOptionsContainer.removeChild(questionOptionsContainer.firstChild);
|
||||
}
|
||||
function resetQuestion() {
|
||||
|
||||
// Reset the question type to the default value
|
||||
questionTypeSelect.value = 'radio';
|
||||
// Clear the title and description fields
|
||||
questionTitleInput.value = '';
|
||||
questionDescriptionInput.value = '';
|
||||
|
||||
// Hide the options configurator if necessary
|
||||
optionsConfigurator.style.display = 'block';
|
||||
// Clear all option input fields
|
||||
while (questionOptionsContainer.firstChild) {
|
||||
questionOptionsContainer.removeChild(questionOptionsContainer.firstChild);
|
||||
}
|
||||
|
||||
// Reset the "Add New Question" button
|
||||
addNewQuestionButton.textContent = 'Add New Question';
|
||||
// Reset the question type to the default value
|
||||
questionTypeSelect.value = 'radio';
|
||||
|
||||
// Hide the options configurator if necessary
|
||||
optionsConfigurator.style.display = 'block';
|
||||
|
||||
// Reset the "Add New Question" button
|
||||
addNewQuestionButton.textContent = 'Add New Question';
|
||||
addNewQuestionButton.onclick = addNewQuestion;
|
||||
|
||||
toastr.success('Question options have been reset.');
|
||||
}
|
||||
// Add a new question
|
||||
addNewQuestionButton.onclick = addNewQuestion;
|
||||
|
||||
toastr.success('Question options have been reset.');
|
||||
});
|
||||
|
||||
// Add a new question
|
||||
addNewQuestionButton.onclick = addNewQuestion;
|
||||
|
||||
function addNewQuestion() {
|
||||
function addNewQuestion() {
|
||||
// Get the current content from the editor
|
||||
try {
|
||||
currentContent = JSON.parse(question_editor.get().text);
|
||||
} catch {
|
||||
currentContent = JSON.parse(JSON.stringify(question_editor.get().json));
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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
|
||||
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.";
|
||||
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,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'text') {
|
||||
newQuestion = {
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "text",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} 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,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} 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
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'yesno') {
|
||||
newQuestion = {
|
||||
enum: ["Yes", "No"],
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "radio",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'truefalse') {
|
||||
newQuestion = {
|
||||
enum: ["True", "False"],
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "radio",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
}
|
||||
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,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'text') {
|
||||
newQuestion = {
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "text",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} 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,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'dropdown-select') {
|
||||
newQuestion = {
|
||||
enum: options.length > 0 ? options : ["Option 1", "Option 2", "Option 3"], // Default options if none provided
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "select2",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} 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
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'yesno') {
|
||||
newQuestion = {
|
||||
enum: ["Yes", "No"],
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "radio",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'truefalse') {
|
||||
newQuestion = {
|
||||
enum: ["True", "False"],
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "radio",
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'textarea') {
|
||||
newQuestion = {
|
||||
type: "string",
|
||||
title: title,
|
||||
format: "textarea", // Use textarea format
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'number') {
|
||||
newQuestion = {
|
||||
type: "number",
|
||||
title: title,
|
||||
description: description,
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
} else if (selectedType === 'range') {
|
||||
newQuestion = {
|
||||
type: "range",
|
||||
title: title,
|
||||
description: description,
|
||||
minimum: 0, // Default minimum value
|
||||
maximum: 100, // Default maximum value
|
||||
questionType: selectedType, // Add questionType property
|
||||
};
|
||||
}
|
||||
|
||||
newQuestion.propertyOrder = current_question_number; // Set the property order
|
||||
|
||||
newQuestion.propertyOrder = current_question_number; // Set the property order
|
||||
// Add the new question to the properties
|
||||
currentContent.properties[`question_${current_question_number}`] = newQuestion;
|
||||
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
|
||||
}
|
||||
question_editor.update({ json: currentContent });
|
||||
toastr.success(`New question added: ${title}`);
|
||||
renderQuestions(); // Re-render the questions list
|
||||
}
|
||||
|
||||
function renderQuestions() {
|
||||
questionsContainer.innerHTML = ''; // Clear the container
|
||||
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 || {};
|
||||
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;
|
||||
});
|
||||
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);
|
||||
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 = `
|
||||
questionElement.innerHTML = `
|
||||
<strong>${question.title || key}</strong>
|
||||
<button type="button" class="btn btn-warning btn-sm edit-question-button" data-question-key="${key}" style="margin-left: 10px;">Edit</button>
|
||||
<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);
|
||||
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);
|
||||
});
|
||||
questionElement.querySelector('.delete-question-button').addEventListener('click', (e) => {
|
||||
const questionKey = e.target.getAttribute('data-question-key');
|
||||
deleteQuestion(questionKey);
|
||||
});
|
||||
|
||||
// Add event listener to edit the question
|
||||
questionElement.querySelector('.edit-question-button').addEventListener('click', (e) => {
|
||||
const questionKey = e.target.getAttribute('data-question-key');
|
||||
editQuestion(questionKey);
|
||||
});
|
||||
questionElement.querySelector('.edit-question-button').addEventListener('click', (e) => {
|
||||
const questionKey = e.target.getAttribute('data-question-key');
|
||||
editQuestion(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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
let draggedElement = null;
|
||||
|
||||
function handleDragStart(e) {
|
||||
draggedElement = this;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/html', this.outerHTML);
|
||||
this.classList.add('dragging');
|
||||
}
|
||||
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 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'));
|
||||
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);
|
||||
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;
|
||||
});
|
||||
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
|
||||
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;
|
||||
});
|
||||
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);
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 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.');
|
||||
}
|
||||
}
|
||||
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();
|
||||
renderQuestions();
|
||||
|
||||
function editQuestion(questionKey) {
|
||||
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 question = properties[questionKey];
|
||||
function editQuestion(questionKey) {
|
||||
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 question = properties[questionKey];
|
||||
|
||||
if (question) {
|
||||
if (question) {
|
||||
// Populate the form fields with the question's data
|
||||
questionTitleInput.value = question.title || '';
|
||||
questionDescriptionInput.value = question.description || '';
|
||||
questionTypeSelect.value = question.questionType || 'text';
|
||||
questionTitleInput.value = question.title || '';
|
||||
questionDescriptionInput.value = question.description || '';
|
||||
questionTypeSelect.value = question.questionType || 'text';
|
||||
|
||||
// Clear existing options and populate them if applicable
|
||||
while (questionOptionsContainer.firstChild) {
|
||||
questionOptionsContainer.removeChild(questionOptionsContainer.firstChild);
|
||||
}
|
||||
if (question.enum) {
|
||||
question.enum.forEach((option, index) => {
|
||||
const optionInput = document.createElement('div');
|
||||
optionInput.className = 'option-input';
|
||||
optionInput.style.marginBottom = '5px';
|
||||
optionInput.innerHTML = `
|
||||
while (questionOptionsContainer.firstChild) {
|
||||
questionOptionsContainer.removeChild(questionOptionsContainer.firstChild);
|
||||
}
|
||||
if (question.enum) {
|
||||
question.enum.forEach((option, index) => {
|
||||
const optionInput = document.createElement('div');
|
||||
optionInput.className = 'option-input';
|
||||
optionInput.style.marginBottom = '5px';
|
||||
optionInput.innerHTML = `
|
||||
<input type="text" class="form-control option-field" value="${option}" placeholder="Option ${index + 1}" 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);
|
||||
questionOptionsContainer.appendChild(optionInput);
|
||||
|
||||
// Add event listener to remove the option
|
||||
optionInput.querySelector('.remove-option-button').addEventListener('click', () => {
|
||||
questionOptionsContainer.removeChild(optionInput);
|
||||
});
|
||||
});
|
||||
optionsConfigurator.style.display = 'block';
|
||||
} else {
|
||||
optionsConfigurator.style.display = 'none';
|
||||
}
|
||||
optionInput.querySelector('.remove-option-button').addEventListener('click', () => {
|
||||
questionOptionsContainer.removeChild(optionInput);
|
||||
});
|
||||
});
|
||||
optionsConfigurator.style.display = 'block';
|
||||
} else {
|
||||
optionsConfigurator.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update the "Add New Question" button to save changes
|
||||
addNewQuestionButton.textContent = 'Save Changes';
|
||||
addNewQuestionButton.onclick = () => {
|
||||
saveEditedQuestion(questionKey);
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error editing question:', error);
|
||||
toastr.error('Failed to edit the question.');
|
||||
}
|
||||
}
|
||||
addNewQuestionButton.textContent = 'Save Changes';
|
||||
addNewQuestionButton.onclick = () => {
|
||||
saveEditedQuestion(questionKey);
|
||||
};
|
||||
document.getElementById('add-question-block').open = true; // Open the question block
|
||||
document.getElementById('add-question-block').scrollIntoView({ behavior: 'smooth', block: "center" }); // Scroll to the question block
|
||||
|
||||
function saveEditedQuestion(questionKey) {
|
||||
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 question = properties[questionKey];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error editing question:', error);
|
||||
toastr.error('Failed to edit the question.');
|
||||
}
|
||||
}
|
||||
|
||||
if (question) {
|
||||
function saveEditedQuestion(questionKey) {
|
||||
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 question = properties[questionKey];
|
||||
|
||||
if (question) {
|
||||
// Update the question with the new values from the form
|
||||
question.title = questionTitleInput.value || `Question ${questionKey}`;
|
||||
question.description = questionDescriptionInput.value || '';
|
||||
question.format = questionTypeSelect.value;
|
||||
question.title = questionTitleInput.value || `Question ${questionKey}`;
|
||||
question.description = questionDescriptionInput.value || '';
|
||||
question.format = questionTypeSelect.value;
|
||||
|
||||
// Update the options if applicable
|
||||
if (question.enum) {
|
||||
question.enum = Array.from(questionOptionsContainer.querySelectorAll('.option-field'))
|
||||
.map((input) => input.value.trim())
|
||||
.filter((option) => option !== ''); // Exclude empty options
|
||||
}
|
||||
if (question.enum) {
|
||||
question.enum = Array.from(questionOptionsContainer.querySelectorAll('.option-field'))
|
||||
.map((input) => input.value.trim())
|
||||
.filter((option) => option !== ''); // Exclude empty options
|
||||
}
|
||||
|
||||
// Update the editor with the modified question
|
||||
currentContent.properties[questionKey] = question;
|
||||
question_editor.update({ json: currentContent });
|
||||
toastr.success(`Question "${question.title}" updated successfully.`);
|
||||
renderQuestions(); // Re-render the questions list
|
||||
currentContent.properties[questionKey] = question;
|
||||
question_editor.update({ json: currentContent });
|
||||
|
||||
toastr.success(`Question "${question.title}" updated successfully.`);
|
||||
renderQuestions(); // Re-render the questions list
|
||||
|
||||
// Reset the "Add New Question" button
|
||||
addNewQuestionButton.textContent = 'Add New Question';
|
||||
addNewQuestionButton.onclick = addNewQuestion;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving edited question:', error);
|
||||
toastr.error('Failed to save the question.');
|
||||
}
|
||||
}
|
||||
});
|
||||
addNewQuestionButton.textContent = 'Add New Question';
|
||||
addNewQuestionButton.onclick = addNewQuestion;
|
||||
resetQuestion(); // Reset the question options
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving edited question:', error);
|
||||
toastr.error('Failed to save the question.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<style>
|
||||
/* Style the <details> block */
|
||||
details {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
background-color:rgba(13, 8, 8, 0.86);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Style the <summary> element */
|
||||
details summary {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Add hover effect to <summary> */
|
||||
details summary:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* Add padding to the content inside <details> */
|
||||
details > *:not(summary) {
|
||||
margin-top: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* Add a border to the expanded <details> */
|
||||
details[open] {
|
||||
border-color: #007bff;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user