WIP: enhance code editor layout and implement live preview functionality

This commit is contained in:
Michael Czechowski
2025-05-18 21:55:49 +02:00
parent d2be51ac49
commit 10094e36dc
4 changed files with 247 additions and 99 deletions

View File

@@ -8,7 +8,8 @@ const state = {
currentModule: null, currentModule: null,
currentLessonIndex: 0, currentLessonIndex: 0,
modules: [], modules: [],
userProgress: {} // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } }
userCodeBeforeValidation: "" // Track user code state before validation
}; };
// DOM elements // DOM elements
@@ -158,6 +159,34 @@ function resetSuccessIndicators() {
elements.taskInstruction.classList.remove("success-instruction"); elements.taskInstruction.classList.remove("success-instruction");
} }
// Configure editor layout based on display type
function configureEditorLayout(lesson) {
// Default to block display if not specified
const displayType = lesson.editorDisplayType || "block";
// Reset classes
elements.codeEditor.classList.remove("inline-editor", "block-editor");
elements.editorContent.classList.remove("inline-mode", "block-mode");
// Apply appropriate layout class
if (displayType === "inline") {
elements.codeEditor.classList.add("inline-editor");
elements.editorContent.classList.add("inline-mode");
// Add special styling for inline mode
elements.codeInput.style.display = "inline-block";
elements.codeInput.style.width = lesson.inlineInputWidth || "auto";
} else {
// Default block mode
elements.codeEditor.classList.add("block-editor");
elements.editorContent.classList.add("block-mode");
// Reset styles for block mode
elements.codeInput.style.display = "block";
elements.codeInput.style.width = "100%";
}
}
// Load the current lesson // Load the current lesson
function loadCurrentLesson() { function loadCurrentLesson() {
if (!state.currentModule || !state.currentModule.lessons) { if (!state.currentModule || !state.currentModule.lessons) {
@@ -189,6 +218,9 @@ function loadCurrentLesson() {
lesson lesson
); );
// Configure editor layout based on lesson settings
configureEditorLayout(lesson);
// Update level indicator // Update level indicator
renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length); renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length);
@@ -204,6 +236,39 @@ function loadCurrentLesson() {
// Focus on the code editor by default // Focus on the code editor by default
elements.codeInput.focus(); elements.codeInput.focus();
// Store current code
state.userCodeBeforeValidation = elements.codeInput.value;
// Track live changes and update preview when the user pauses typing
setupLivePreview();
}
// Setup live preview functionality
let previewTimer = null;
function setupLivePreview() {
// Clear previous event listener if any
elements.codeInput.removeEventListener('input', handleUserInput);
// Add new event listener
elements.codeInput.addEventListener('input', handleUserInput);
}
// Handle user input with debounced preview updates
function handleUserInput() {
// Clear the previous timer
if (previewTimer) {
clearTimeout(previewTimer);
}
// Set a new timer for preview update after user stops typing
previewTimer = setTimeout(() => {
// Apply the code for preview without validation
lessonEngine.applyUserCode(elements.codeInput.value);
}, 500); // Update preview 500ms after user stops typing
// Store current code state
state.userCodeBeforeValidation = elements.codeInput.value;
} }
// Update navigation buttons state // Update navigation buttons state
@@ -248,6 +313,9 @@ function runCode() {
const userCode = elements.codeInput.value; const userCode = elements.codeInput.value;
const lesson = state.currentModule.lessons[state.currentLessonIndex]; const lesson = state.currentModule.lessons[state.currentLessonIndex];
// Always apply the code to the preview, regardless of validation result
lessonEngine.applyUserCode(userCode, true);
const validationResult = validateUserCode(userCode, lesson); const validationResult = validateUserCode(userCode, lesson);
if (validationResult.isValid) { if (validationResult.isValid) {
@@ -268,9 +336,6 @@ function runCode() {
elements.nextBtn.classList.add("success"); elements.nextBtn.classList.add("success");
elements.taskInstruction.classList.add("success-instruction"); elements.taskInstruction.classList.add("success-instruction");
// Apply the code to see the result
lessonEngine.applyUserCode(userCode);
// Enable the next button if not already on the last lesson // Enable the next button if not already on the last lesson
if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { if (state.currentLessonIndex < state.currentModule.lessons.length - 1) {
elements.nextBtn.disabled = false; elements.nextBtn.disabled = false;
@@ -358,7 +423,7 @@ function showHelp() {
<h4>Controls</h4> <h4>Controls</h4>
<ul> <ul>
<li><strong>Run</strong> - Test your CSS code</li> <li><strong>Run</strong> - Test your CSS code and apply it to the preview</li>
<li><strong>Previous/Next</strong> - Navigate between lessons</li> <li><strong>Previous/Next</strong> - Navigate between lessons</li>
<li><strong>Progress</strong> - Select a different learning module</li> <li><strong>Progress</strong> - Select a different learning module</li>
<li><strong>Reset Progress</strong> - Clear all your saved progress</li> <li><strong>Reset Progress</strong> - Clear all your saved progress</li>
@@ -366,7 +431,8 @@ function showHelp() {
<h4>Tips</h4> <h4>Tips</h4>
<ul> <ul>
<li>Use the preview area to see how your CSS affects the elements</li> <li>Your code changes will automatically preview as you type</li>
<li>The preview area shows how your CSS affects the elements</li>
<li>Your progress is automatically saved in your browser storage</li> <li>Your progress is automatically saved in your browser storage</li>
<li>You can revisit completed lessons at any time</li> <li>You can revisit completed lessons at any time</li>
<li>Press Tab in the code editor to indent with two spaces</li> <li>Press Tab in the code editor to indent with two spaces</li>

View File

@@ -16,16 +16,16 @@ import responsiveConfig from "../../lessons/08-responsive.json";
// Module store // Module store
const moduleStore = [ const moduleStore = [
basicSelectorsConfig basicSelectorsConfig,
// basicsConfig, basicsConfig,
// boxModelConfig, boxModelConfig,
// selectorsConfig, selectorsConfig,
// colorsConfig, colorsConfig,
// typographyConfig, typographyConfig,
// unitVariablesConfig, unitVariablesConfig,
// transitionsAnimationsConfig, transitionsAnimationsConfig,
// layoutConfig, layoutConfig,
// responsiveConfig responsiveConfig
]; ];
/** /**
@@ -83,6 +83,15 @@ function validateModuleConfig(config) {
config.lessons.forEach((lesson, index) => { config.lessons.forEach((lesson, index) => {
if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`); if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`);
if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`); if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`);
// Apply defaults for new properties if they don't exist
if (lesson.editorDisplayType === undefined) {
lesson.editorDisplayType = "block"; // Default to block display
}
if (lesson.editorDisplayType === "inline" && lesson.inlineInputWidth === undefined) {
lesson.inlineInputWidth = "auto"; // Default width for inline input
}
}); });
} }
@@ -111,3 +120,36 @@ export function addCustomModule(moduleConfig) {
return false; return false;
} }
} }
/**
* Convert a module to include the enhanced schema with editorDisplayType
* @param {Object} moduleConfig - The module configuration to convert
* @returns {Object} The enhanced module configuration
*/
export function enhanceModuleSchema(moduleConfig) {
if (!moduleConfig || !moduleConfig.lessons) return moduleConfig;
const enhancedModule = {...moduleConfig};
enhancedModule.lessons = moduleConfig.lessons.map(lesson => {
const enhancedLesson = {...lesson};
// Apply defaults for new properties if they don't exist
if (enhancedLesson.editorDisplayType === undefined) {
enhancedLesson.editorDisplayType = "block"; // Default to block display
}
if (enhancedLesson.editorDisplayType === "inline" && enhancedLesson.inlineInputWidth === undefined) {
enhancedLesson.inlineInputWidth = "auto"; // Default width for inline input
}
return enhancedLesson;
});
return enhancedModule;
}
// Enhance all modules on load to ensure they have the new schema properties
moduleStore.forEach((module, index) => {
moduleStore[index] = enhanceModuleSchema(module);
});

View File

@@ -11,6 +11,7 @@ export class LessonEngine {
this.userCode = ""; this.userCode = "";
this.currentModule = null; this.currentModule = null;
this.currentLessonIndex = 0; this.currentLessonIndex = 0;
this.lastRenderedCode = ""; // Track last applied code to prevent unnecessary re-renders
} }
/** /**
@@ -32,6 +33,7 @@ export class LessonEngine {
setLesson(lesson) { setLesson(lesson) {
this.currentLesson = lesson; this.currentLesson = lesson;
this.userCode = lesson.initialCode || ""; this.userCode = lesson.initialCode || "";
this.lastRenderedCode = ""; // Reset last rendered code
this.renderPreview(); this.renderPreview();
} }
@@ -73,13 +75,35 @@ export class LessonEngine {
/** /**
* Apply user-written CSS to the preview area * Apply user-written CSS to the preview area
* @param {string} code - User CSS code * @param {string} code - User CSS code
* @param {boolean} forceUpdate - Force update the preview even if code hasn't changed
*/ */
applyUserCode(code) { applyUserCode(code, forceUpdate = false) {
if (!this.currentLesson) return; if (!this.currentLesson) return;
this.userCode = code; this.userCode = code;
// Only re-render if code changed or forced update
if (forceUpdate || this.lastRenderedCode !== code) {
this.lastRenderedCode = code;
this.renderPreview(); this.renderPreview();
} }
}
/**
* Get the complete CSS by combining all parts
* @returns {string} The complete CSS
*/
getCompleteCss() {
if (!this.currentLesson) return "";
const { codePrefix, codeSuffix } = this.currentLesson;
return `
${codePrefix || ""}
${this.userCode || ""}
${codeSuffix || ""}
`;
}
/** /**
* Render the preview for the current lesson * Render the preview for the current lesson
@@ -103,13 +127,16 @@ export class LessonEngine {
container.innerHTML = ""; container.innerHTML = "";
container.appendChild(iframe); container.appendChild(iframe);
// Get the complete CSS by combining all parts
const userCssWithWrapper = this.getCompleteCss();
// Create the complete CSS by combining base CSS with user code and sandbox CSS // Create the complete CSS by combining base CSS with user code and sandbox CSS
const combinedCSS = ` const combinedCSS = `
/* Base CSS */ /* Base CSS */
${previewBaseCSS || ""} ${previewBaseCSS || ""}
/* User Code */ /* User Code */
${this.userCode || ""} ${userCssWithWrapper || ""}
/* Sandbox CSS (for visualizing the exercise) */ /* Sandbox CSS (for visualizing the exercise) */
${sandboxCSS || ""} ${sandboxCSS || ""}
@@ -176,7 +203,7 @@ export class LessonEngine {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}; };
localStorage.setItem("cssQuest_progress", JSON.stringify(progressData)); localStorage.setItem("codeCrispies.progress", JSON.stringify(progressData));
} }
/** /**
@@ -185,7 +212,7 @@ export class LessonEngine {
* @returns {Object|null} Loaded progress data or null if not found * @returns {Object|null} Loaded progress data or null if not found
*/ */
loadProgress(modules) { loadProgress(modules) {
const savedProgress = localStorage.getItem("cssQuest_progress"); const savedProgress = localStorage.getItem("codeCrispies.progress");
if (!savedProgress) return null; if (!savedProgress) return null;
try { try {
@@ -225,6 +252,6 @@ export class LessonEngine {
* Clear all saved progress * Clear all saved progress
*/ */
clearProgress() { clearProgress() {
localStorage.removeItem("cssQuest_progress"); localStorage.removeItem("codeCrispies.progress");
} }
} }

View File

@@ -271,6 +271,15 @@ code {
overflow: hidden; overflow: hidden;
} }
.code-editor.block-editor {
display: block;
}
.code-editor.inline-editor {
display: inline-block;
width: 100%;
}
.editor-header { .editor-header {
background-color: var(--code-bg); background-color: var(--code-bg);
padding: var(--spacing-xs) var(--spacing-md); padding: var(--spacing-xs) var(--spacing-md);
@@ -283,11 +292,13 @@ code {
} }
.editor-content { .editor-content {
display: flex;
flex-direction: column;
background-color: var(--editor-bg); background-color: var(--editor-bg);
color: #d4d4d4; color: #d4d4d4;
padding: var(--spacing-md); padding: var(--spacing-md);
margin-bottom: 4rem; /*margin-bottom: 4rem;*/
overflow-y: auto; overflow-y: scroll;
height: 100%; height: 100%;
font-family: var(--font-code); font-family: var(--font-code);
font-size: 14px; font-size: 14px;
@@ -314,6 +325,8 @@ code {
} }
.code-input { .code-input {
flex: 1;
display: block;
background-color: transparent; background-color: transparent;
color: #d4d4d4; color: #d4d4d4;
border: none; border: none;