feat: add undo/redo/reset editor tools with keyboard shortcuts
- Add history extension to CodeMirror for undo/redo support - Ctrl+Z for undo, Ctrl+Shift+Z for redo now work - Add toolbar buttons: ↶ Undo, ↷ Redo, ⟲ Reset - Reset button restores editor to initial lesson code - Add .btn-icon and .editor-tools CSS styles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
25
src/app.js
25
src/app.js
@@ -23,6 +23,9 @@ const elements = {
|
|||||||
taskInstruction: document.getElementById("task-instruction"),
|
taskInstruction: document.getElementById("task-instruction"),
|
||||||
codeInput: document.getElementById("code-input"),
|
codeInput: document.getElementById("code-input"),
|
||||||
runBtn: document.getElementById("run-btn"),
|
runBtn: document.getElementById("run-btn"),
|
||||||
|
undoBtn: document.getElementById("undo-btn"),
|
||||||
|
redoBtn: document.getElementById("redo-btn"),
|
||||||
|
resetCodeBtn: document.getElementById("reset-code-btn"),
|
||||||
hintArea: document.getElementById("hint-area"),
|
hintArea: document.getElementById("hint-area"),
|
||||||
validationIndicators: document.querySelector(".validation-indicators-container"),
|
validationIndicators: document.querySelector(".validation-indicators-container"),
|
||||||
editorContent: document.querySelector(".editor-content"),
|
editorContent: document.querySelector(".editor-content"),
|
||||||
@@ -376,6 +379,19 @@ function prevLesson() {
|
|||||||
|
|
||||||
// ================= CODE EXECUTION =================
|
// ================= CODE EXECUTION =================
|
||||||
|
|
||||||
|
function resetCode() {
|
||||||
|
// Reset editor to initial code for current lesson
|
||||||
|
lessonEngine.reset();
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
if (codeEditor && engineState.lesson) {
|
||||||
|
codeEditor.setValue(engineState.lesson.initialCode || "");
|
||||||
|
}
|
||||||
|
// Clear hints and success indicators
|
||||||
|
clearHint();
|
||||||
|
resetSuccessIndicators();
|
||||||
|
elements.validationIndicators.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
function runCode() {
|
function runCode() {
|
||||||
const userCode = codeEditor ? codeEditor.getValue() : "";
|
const userCode = codeEditor ? codeEditor.getValue() : "";
|
||||||
|
|
||||||
@@ -561,6 +577,15 @@ function init() {
|
|||||||
elements.nextBtn.addEventListener("click", nextLesson);
|
elements.nextBtn.addEventListener("click", nextLesson);
|
||||||
elements.runBtn.addEventListener("click", runCode);
|
elements.runBtn.addEventListener("click", runCode);
|
||||||
|
|
||||||
|
// Editor tools
|
||||||
|
elements.undoBtn.addEventListener("click", () => {
|
||||||
|
if (codeEditor) codeEditor.undo();
|
||||||
|
});
|
||||||
|
elements.redoBtn.addEventListener("click", () => {
|
||||||
|
if (codeEditor) codeEditor.redo();
|
||||||
|
});
|
||||||
|
elements.resetCodeBtn.addEventListener("click", resetCode);
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
elements.helpBtn.addEventListener("click", showHelp);
|
elements.helpBtn.addEventListener("click", showHelp);
|
||||||
elements.modalClose.addEventListener("click", closeModal);
|
elements.modalClose.addEventListener("click", closeModal);
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
import { EditorState, Prec } from "@codemirror/state";
|
import { EditorState, Prec } from "@codemirror/state";
|
||||||
import { EditorView, keymap, placeholder } from "@codemirror/view";
|
import { EditorView, keymap, placeholder } from "@codemirror/view";
|
||||||
import { defaultKeymap, indentMore, indentLess } from "@codemirror/commands";
|
import { defaultKeymap, historyKeymap, indentMore, indentLess, undo, redo } from "@codemirror/commands";
|
||||||
|
import { history } from "@codemirror/commands";
|
||||||
import { oneDark } from "@codemirror/theme-one-dark";
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
import { html } from "@codemirror/lang-html";
|
import { html } from "@codemirror/lang-html";
|
||||||
import { css } from "@codemirror/lang-css";
|
import { css } from "@codemirror/lang-css";
|
||||||
@@ -49,6 +50,8 @@ export class CodeEditor {
|
|||||||
langExtension,
|
langExtension,
|
||||||
oneDark,
|
oneDark,
|
||||||
editorTheme,
|
editorTheme,
|
||||||
|
// History for undo/redo
|
||||||
|
history(),
|
||||||
// Emmet abbreviation tracking
|
// Emmet abbreviation tracking
|
||||||
abbreviationTracker(),
|
abbreviationTracker(),
|
||||||
// High priority keymap for Emmet
|
// High priority keymap for Emmet
|
||||||
@@ -58,8 +61,9 @@ export class CodeEditor {
|
|||||||
run: expandAbbreviation
|
run: expandAbbreviation
|
||||||
}
|
}
|
||||||
])),
|
])),
|
||||||
// Standard keymaps
|
// Standard keymaps including history (Ctrl+Z, Ctrl+Shift+Z)
|
||||||
keymap.of([
|
keymap.of([
|
||||||
|
...historyKeymap,
|
||||||
{ key: "Tab", run: indentMore },
|
{ key: "Tab", run: indentMore },
|
||||||
{ key: "Shift-Tab", run: indentLess },
|
{ key: "Shift-Tab", run: indentLess },
|
||||||
...defaultKeymap
|
...defaultKeymap
|
||||||
@@ -138,6 +142,24 @@ export class CodeEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo last change
|
||||||
|
*/
|
||||||
|
undo() {
|
||||||
|
if (this.view) {
|
||||||
|
undo(this.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redo last undone change
|
||||||
|
*/
|
||||||
|
redo() {
|
||||||
|
if (this.view) {
|
||||||
|
redo(this.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the editor
|
* Destroy the editor
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
<label for="code-input" class="editor-label">CSS Editor</label>
|
<label for="code-input" class="editor-label">CSS Editor</label>
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
|
<div class="editor-tools">
|
||||||
|
<button id="undo-btn" class="btn btn-icon" title="Undo (Ctrl+Z)">↶</button>
|
||||||
|
<button id="redo-btn" class="btn btn-icon" title="Redo (Ctrl+Shift+Z)">↷</button>
|
||||||
|
<button id="reset-code-btn" class="btn btn-icon" title="Reset to initial code">⟲</button>
|
||||||
|
</div>
|
||||||
<div class="validation-indicators-container"></div>
|
<div class="validation-indicators-container"></div>
|
||||||
<button id="run-btn" class="btn btn-run">
|
<button id="run-btn" class="btn btn-run">
|
||||||
<img src="./gear.svg" alt="" />Run
|
<img src="./gear.svg" alt="" />Run
|
||||||
|
|||||||
21
src/main.css
21
src/main.css
@@ -712,6 +712,27 @@ code, kbd {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
min-width: 32px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--light-text);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-tools {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--light-text);
|
color: var(--light-text);
|
||||||
|
|||||||
Reference in New Issue
Block a user