diff --git a/functions.php b/functions.php index e2c15b8..212a449 100644 --- a/functions.php +++ b/functions.php @@ -9,9 +9,10 @@ function quiztech_theme_enqueue_scripts() { 'template-manage-questions.php', 'template-manage-credits.php', 'template-view-results.php', + 'template-assessment-builder.php', // Add Assessment Builder template ]; - // Check if the current page is using one of our templates + // Check if the current page is using one of our general Quiztech templates $is_quiztech_template = false; foreach ( $quiztech_templates as $template ) { if ( is_page_template( $template ) ) { @@ -46,6 +47,33 @@ function quiztech_theme_enqueue_scripts() { 'job_added_success' => esc_html__( 'Job added successfully!', 'quiztech' ), ]); } + + // --- Enqueue script specifically for Assessment Builder --- + if ( is_page_template( 'template-assessment-builder.php' ) ) { + $builder_script_path = get_stylesheet_directory() . '/js/quiztech-assessment-builder.js'; + $builder_script_url = get_stylesheet_directory_uri() . '/js/quiztech-assessment-builder.js'; + $builder_version = file_exists( $builder_script_path ) ? filemtime( $builder_script_path ) : '1.0'; + + wp_enqueue_script( + 'quiztech-builder-script', // Unique handle + $builder_script_url, + array( 'jquery', 'jquery-ui-sortable' ), // Depends on jQuery and Sortable for drag/drop later + $builder_version, + true // Load in footer + ); + + // Localize data specifically for the builder script + wp_localize_script( 'quiztech-builder-script', 'quiztechBuilderData', [ + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'fetch_nonce' => wp_create_nonce( 'quiztech_fetch_library_questions_action' ), // TODO: Verify action name in AJAX handler + 'save_nonce' => wp_create_nonce( 'quiztech_save_assessment_action' ), // TODO: Verify action name in AJAX handler + 'error_generic' => esc_html__( 'An error occurred. Please try again.', 'quiztech' ), + 'loading_questions' => esc_html__( 'Loading questions...', 'quiztech' ), + 'saving_assessment' => esc_html__( 'Saving assessment...', 'quiztech' ), + 'assessment_saved' => esc_html__( 'Assessment saved successfully!', 'quiztech' ), + // Add more localized strings as needed + ]); + } } add_action( 'wp_enqueue_scripts', 'quiztech_theme_enqueue_scripts' ); diff --git a/js/quiztech-assessment-builder.js b/js/quiztech-assessment-builder.js new file mode 100644 index 0000000..b3c1061 --- /dev/null +++ b/js/quiztech-assessment-builder.js @@ -0,0 +1,314 @@ +jQuery(document).ready(function($) { + console.log('Assessment Builder Script Loaded'); // Debugging + + // --- Cache DOM Elements --- + const $libraryList = $('#library-questions-list'); + const $currentList = $('#current-assessment-questions'); + const $assessmentTitleInput = $('#assessment-title'); + const $totalCostSpan = $('#assessment-total-cost'); + const $saveButton = $('#save-assessment-button'); + const $saveSpinner = $saveButton.siblings('.spinner'); + const $saveStatus = $('#save-status'); + + // --- State --- + let currentAssessmentQuestions = []; // Array to hold IDs of questions in the right pane + let currentAssessmentId = null; // Store the ID if editing an existing assessment (TODO: Load this if editing) + + // --- Functions --- + + /** + * Updates the total cost display based on the questions currently in the assessment list. + */ + function updateTotalCost() { + let totalCost = 0; + $currentList.find('.selected-question-item').each(function() { + totalCost += parseInt($(this).data('cost') || 0); + }); + $totalCostSpan.text(totalCost); + } + + /** + * Updates the internal `currentAssessmentQuestions` array based on the current DOM order. + */ + function updateStateFromDOM() { + currentAssessmentQuestions = $currentList.find('.selected-question-item').map(function() { + return $(this).data('question-id'); + }).get(); // .get() converts jQuery object to array + console.log('Updated State:', currentAssessmentQuestions); // Debugging + } + + + /** + * Fetches questions for the library pane via AJAX. + * TODO: Add parameters for search, filters, pagination. + */ + function fetchLibraryQuestions(page = 1, search = '') { + console.log('Fetching library questions...'); + $libraryList.html('

' + quiztechBuilderData.loading_questions + '

'); // Show loading indicator + + $.post(quiztechBuilderData.ajax_url, { + action: 'quiztech_fetch_library_questions', + // nonce: quiztechBuilderData.fetch_nonce, // TODO: Implement nonce verification in PHP + page: page, + search: search + }) + .done(function(response) { + if (response.success && response.data.questions) { + renderLibraryQuestions(response.data.questions); + // TODO: Render pagination controls using response.data.pagination + } else { + $libraryList.html('

' + (response.data.message || quiztechBuilderData.error_generic) + '

'); + } + }) + .fail(function() { + $libraryList.html('

' + quiztechBuilderData.error_generic + '

'); + }) + .always(function() { + // Hide loading indicator if needed + }); + } + + /** + * Renders the fetched questions into the library list container. + * @param {Array} questions Array of question objects {id, title, type, cost}. + */ + function renderLibraryQuestions(questions) { + $libraryList.empty(); // Clear previous content or loading indicator + if (questions.length === 0) { + $libraryList.html('

' + 'No questions found.' + '

'); // TODO: Localize + return; + } + + questions.forEach(function(q) { + // Check if question is already in the current assessment + const isAdded = currentAssessmentQuestions.includes(q.id); + const buttonHtml = isAdded + ? '' // TODO: Localize + : ''; // TODO: Localize + + const itemHtml = ` +
+ + ${q.title || 'Untitled'} (Type: ${q.type || 'N/A'}) [Cost: ${q.cost || 0}] + + ${buttonHtml} +
+ `; + $libraryList.append(itemHtml); + }); + + // --- Initialize Draggable on newly rendered library items --- + initializeDraggable(); + // --- --- --- --- --- + + } + + /** + * Initializes draggable functionality on library items. + */ + function initializeDraggable() { + $libraryList.find('.library-question-item').draggable({ + connectToSortable: '#current-assessment-questions', + helper: 'clone', + revert: 'invalid', + start: function(event, ui) { + ui.helper.css('width', $(this).width()); // Maintain width during drag + ui.helper.addClass('dragging-helper'); // Optional: for styling the helper + }, + stop: function(event, ui) { + ui.helper.removeClass('dragging-helper'); + } + }); + } + + + /** + * Helper to find question data in the currently rendered library list. + * Note: This is inefficient if the library is paginated. A better approach + * would be to store added question details in a separate object. + * @param {number} questionId + * @returns {object|null} Object with title, cost etc. or null if not found in current view. + */ + function findQuestionDataInLibrary(questionId) { + const $item = $libraryList.find(`.library-question-item[data-question-id="${questionId}"]`); + if ($item.length) { + return { + id: questionId, + title: $item.find('strong').text(), + cost: $item.data('cost') + // Extract type if needed + }; + } + return null; // Not found in the current library view + } + + + /** + * Updates the 'Add' buttons in the library list, disabling those for questions + * already present in the current assessment. + */ + function updateLibraryButtons() { + $libraryList.find('.add-question-to-assessment').each(function() { + const $button = $(this); + const questionId = $button.data('question-id'); + if (currentAssessmentQuestions.includes(questionId)) { + $button.prop('disabled', true).text('Added'); // TODO: Localize + } else { + $button.prop('disabled', false).text('Add'); // TODO: Localize + } + }); + } + + /** + * Saves the current assessment (title and question IDs) via AJAX. + */ + function saveAssessment() { + console.log('Saving assessment...'); + $saveSpinner.css('visibility', 'visible'); + $saveButton.prop('disabled', true); + $saveStatus.empty().removeClass('success error'); + + const assessmentTitle = $assessmentTitleInput.val(); + if (!assessmentTitle) { + alert('Assessment title cannot be empty.'); // TODO: Use a nicer notification + $saveSpinner.css('visibility', 'hidden'); + $saveButton.prop('disabled', false); + return; + } + + $.post(quiztechBuilderData.ajax_url, { + action: 'quiztech_save_assessment', + // nonce: quiztechBuilderData.save_nonce, // TODO: Implement nonce verification in PHP + assessment_id: currentAssessmentId, // Will be null for new assessments + title: assessmentTitle, + question_ids: currentAssessmentQuestions + }) + .done(function(response) { + if (response.success && response.data.assessment_id) { + currentAssessmentId = response.data.assessment_id; // Store the ID after saving + $saveStatus.text(quiztechBuilderData.assessment_saved).addClass('success'); + // Optionally redirect or update UI further + } else { + $saveStatus.text(response.data.message || quiztechBuilderData.error_generic).addClass('error'); + } + }) + .fail(function() { + $saveStatus.text(quiztechBuilderData.error_generic).addClass('error'); + }) + .always(function() { + $saveSpinner.css('visibility', 'hidden'); + $saveButton.prop('disabled', false); + }); + } + + + // --- Event Handlers --- + + // Add question from library to current assessment (Handles manual click - less relevant with drag/drop but keep for now) + $libraryList.on('click', '.add-question-to-assessment', function() { + const $button = $(this); + const questionId = $button.data('question-id'); + const $item = $button.closest('.library-question-item'); + + if (!currentAssessmentQuestions.includes(questionId)) { + // Clone the item, modify it, and append to the current list + const $clonedItem = $item.clone(); + $clonedItem.removeClass('library-question-item').addClass('selected-question-item'); + $clonedItem.find('.add-question-to-assessment').replaceWith( + `` // TODO: Localize + ); + + // Remove placeholder if it exists + $currentList.find('p').remove(); + + $currentList.append($clonedItem); + + // Update state and UI + updateStateFromDOM(); + updateTotalCost(); + updateLibraryButtons(); + } + }); + + // Remove question from current assessment + $currentList.on('click', '.remove-question-from-assessment', function() { + const $button = $(this); + const questionId = $button.data('question-id'); + $button.closest('.selected-question-item').remove(); // Remove item from DOM + + // Update state and UI + updateStateFromDOM(); + updateTotalCost(); + updateLibraryButtons(); // Re-enable the 'Add' button in the library + + // Add placeholder if list becomes empty + if ($currentList.children().length === 0) { + $currentList.html('

' + 'No questions added yet.' + '

'); // TODO: Localize + } + }); + + // Save assessment button click + $saveButton.on('click', function(e) { + e.preventDefault(); + saveAssessment(); + }); + + // TODO: Add handlers for search/filter/pagination controls when implemented. + // --- Initialize Sortable --- + $currentList.sortable({ + placeholder: "ui-state-highlight", // Class for placeholder styling + axis: 'y', // Allow vertical sorting only + update: function(event, ui) { + // This fires when sorting stops *within* the list OR when an item is received + console.log('Sortable Updated'); + updateStateFromDOM(); + updateTotalCost(); // Recalculate cost based on new order/items + }, + receive: function(event, ui) { + // This fires specifically when a draggable is dropped onto the sortable + console.log('Sortable Received'); + const questionId = ui.item.data('question-id'); + const $originalItem = ui.item; // This is the helper clone that becomes the list item + + // Check for duplicates based on the state *before* this item was potentially added by 'update' + const existingIds = $currentList.find('.selected-question-item').map(function() { + return $(this).data('question-id'); + }).get(); + let count = 0; + existingIds.forEach(id => { if (id === questionId) count++; }); + + if (count > 1) { // If the dropped item creates a duplicate + console.log('Duplicate detected, cancelling drop'); + ui.sender.sortable('cancel'); // Cancel the drop/receive operation + $originalItem.remove(); // Remove the duplicate item that was briefly added + alert('This question is already in the assessment.'); // TODO: Nicer notification + return; // Stop further processing for this event + } + + // Transform the received item (clone) into a selected item + $originalItem.removeClass('library-question-item dragging-helper').addClass('selected-question-item'); + $originalItem.find('.add-question-to-assessment').replaceWith( + `` // TODO: Localize + ); + $originalItem.css({ width: '', height: '' }); // Reset helper styles + + // Remove placeholder if it exists + $currentList.find('p').remove(); + + // Update state and UI (update event will also fire, but good to be explicit) + updateStateFromDOM(); + updateTotalCost(); + updateLibraryButtons(); // Disable the 'Add' button in the library + } + }).disableSelection(); // Prevent text selection while dragging + + // --- Initial Load --- + fetchLibraryQuestions(); // Load initial questions into the library (will also init draggable) + // renderCurrentAssessment(); // Don't render initially, sortable handles it + + // Add placeholder initially if list is empty + if ($currentList.children().length === 0) { + $currentList.html('

' + 'No questions added yet.' + '

'); // TODO: Localize + } + +}); \ No newline at end of file diff --git a/style.css b/style.css index a07fbf4..3b2ef87 100644 --- a/style.css +++ b/style.css @@ -8,3 +8,111 @@ Version: 0.1 */ + + + +/* Assessment Builder Styles */ +.quiztech-assessment-builder-area .entry-content { + max-width: none; /* Allow content to take full width if needed */ +} + +#assessment-builder-container { + display: flex; + flex-wrap: wrap; /* Allow wrapping on smaller screens */ + gap: 20px; /* Space between panes */ + margin-top: 20px; +} + +#assessment-builder-library { + flex: 1 1 300px; /* Flex-grow, flex-shrink, flex-basis */ + min-width: 280px; /* Minimum width before wrapping */ + border: 1px solid #ddd; + padding: 15px; + background-color: #f9f9f9; + box-sizing: border-box; +} + +#assessment-builder-current { + flex: 2 1 500px; /* Takes up more space */ + min-width: 400px; + border: 1px solid #ddd; + padding: 15px; + background-color: #fff; + box-sizing: border-box; +} + +#library-questions-list, +#current-assessment-questions { + min-height: 200px; /* Ensure panes have some height */ + max-height: 400px; /* Limit height and allow scrolling if needed */ + overflow-y: auto; /* Add scrollbar if content exceeds max-height */ + border: 1px dashed #eee; + padding: 10px; + margin-top: 10px; + margin-bottom: 15px; +} + +/* Add some basic styling for list items (placeholders) */ +.library-question-item, +.selected-question-item { + padding: 8px; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; +} + +.library-question-item:last-child, +.selected-question-item:last-child { + border-bottom: none; +} + +#assessment-summary { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +/* Responsive adjustments (optional example) */ +@media (max-width: 768px) { + #assessment-builder-container { + flex-direction: column; + } + #assessment-builder-library, + #assessment-builder-current { + flex-basis: auto; /* Reset basis when stacked */ + min-width: 0; + } +} + + + +/* Drag and Drop Styles */ +.ui-sortable-placeholder { + border: 1px dashed #ccc; + background-color: #f0f0f0; + height: 40px; /* Adjust height to match item height */ + margin-bottom: 8px; /* Match item margin/padding */ + visibility: visible !important; /* Ensure placeholder is visible */ +} + +.dragging-helper { + opacity: 0.8; + border: 1px dashed #aaa; + background-color: #fff; + z-index: 9999; /* Ensure helper is on top */ +} + +/* Style for selected items in the right pane */ +#current-assessment-questions .selected-question-item { + cursor: move; /* Indicate items are draggable/sortable */ + background-color: #fefefe; +} + +/* Optional: Style adjustments for library items */ +#assessment-builder-library .library-question-item { + cursor: grab; +} +#assessment-builder-library .library-question-item:active { + cursor: grabbing; +} diff --git a/template-assessment-builder.php b/template-assessment-builder.php new file mode 100644 index 0000000..8e450cc --- /dev/null +++ b/template-assessment-builder.php @@ -0,0 +1,92 @@ + + +
+
+ +
> +
+ ', '' ); ?> +
+ +
+ +
+ + +
+

+

+
+ + +
+
+

+

+
+ + +
+

+

+
+ +

+
+

+ + +
+
+
+ + 0 +
+

+ + + +

+
+ +
+ +
+ +
+ +
+
+ +