feat: redesign snippet editor form and enhance CodeMirror with advanced editing tools and persistence

This commit is contained in:
2026-07-13 15:15:25 +07:00
parent 324f3a505b
commit af82bfc481
3 changed files with 67 additions and 25 deletions
@@ -32,9 +32,9 @@ public class ManageSnippetController {
@PostMapping("/create") @PostMapping("/create")
public String createSnippet(@ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) { public String createSnippet(@ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) {
snippetService.save(snippet); HtmlSnippet saved = snippetService.save(snippet);
redirectAttributes.addFlashAttribute("successMessage", "Snippet created successfully."); redirectAttributes.addFlashAttribute("successMessage", "Snippet created successfully.");
return "redirect:/manage/snippets"; return "redirect:/manage/snippets/" + saved.getId() + "/edit";
} }
@GetMapping("/{id}/edit") @GetMapping("/{id}/edit")
@@ -52,9 +52,9 @@ public class ManageSnippetController {
@PostMapping("/{id}") @PostMapping("/{id}")
public String updateSnippet(@PathVariable Long id, @ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) { public String updateSnippet(@PathVariable Long id, @ModelAttribute HtmlSnippet snippet, RedirectAttributes redirectAttributes) {
snippet.setId(id); snippet.setId(id);
snippetService.save(snippet); HtmlSnippet saved = snippetService.save(snippet);
redirectAttributes.addFlashAttribute("successMessage", "Snippet updated successfully."); redirectAttributes.addFlashAttribute("successMessage", "Snippet updated successfully.");
return "redirect:/manage/snippets"; return "redirect:/manage/snippets/" + saved.getId() + "/edit";
} }
@PostMapping("/{id}/delete") @PostMapping("/{id}/delete")
@@ -60,10 +60,4 @@
/* Ensures it sits above the video */ /* Ensures it sits above the video */
} }
/* Custom Color Utilities */
.text-old-brick {
color: var(--color-old-brick) !important;
}
.bg-old-brick {
background-color: var(--color-old-brick) !important;
}
@@ -8,6 +8,8 @@
<!-- CodeMirror CSS for beautiful code editing --> <!-- CodeMirror CSS for beautiful code editing -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/monokai.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/monokai.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/dialog/dialog.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/fold/foldgutter.min.css">
<style> <style>
/* ── GrapesJS Layout Overrides ── */ /* ── GrapesJS Layout Overrides ── */
@@ -112,6 +114,16 @@
} }
.gjs-clm-tags { padding: 5px; } .gjs-clm-tags { padding: 5px; }
.gjs-layer-name { font-size: 12px; } .gjs-layer-name { font-size: 12px; }
/* CodeMirror Editor Height */
.CodeMirror {
height: 75vh !important;
min-height: 550px !important;
border: 1px solid #d1d3e2;
border-radius: 0.35rem;
font-size: 14px;
font-family: Consolas, "Courier New", monospace;
}
</style> </style>
</head> </head>
<body> <body>
@@ -129,21 +141,13 @@
<input type="hidden" th:field="*{createdDate}" /> <input type="hidden" th:field="*{createdDate}" />
<div class="row"> <div class="row">
<div class="col-12"> <!-- Main Content Column -->
<div class="col-xl-9 col-lg-8">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Snippet Details</h6> <h6 class="m-0 font-weight-bold text-primary">HTML Content</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" th:field="*{name}" required placeholder="E.g., Footer Contact Text">
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input type="text" class="form-control" id="slug" th:field="*{slug}" required placeholder="E.g., footer-contact">
<small class="form-text text-muted">A unique identifier used to call this snippet in templates. Use lowercase letters and hyphens.</small>
</div>
<div class="form-group"> <div class="form-group">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<label for="snippetContent" class="mb-0">HTML Content</label> <label for="snippetContent" class="mb-0">HTML Content</label>
@@ -177,12 +181,23 @@
</div> </div>
</div> </div>
<div class="col-12"> <!-- Sidebar Publish Column -->
<div class="col-xl-3 col-lg-4">
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-header py-3"> <div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Publish</h6> <h6 class="m-0 font-weight-bold text-primary">Settings & Publish</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" th:field="*{name}" required placeholder="E.g., Footer Contact Text">
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input type="text" class="form-control" id="slug" th:field="*{slug}" required placeholder="E.g., footer-contact">
<small class="form-text text-muted">A unique identifier.</small>
</div>
<hr>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="active" th:field="*{active}"> <input type="checkbox" class="custom-control-input" id="active" th:field="*{active}">
@@ -212,6 +227,23 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/javascript/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/javascript/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/htmlmixed/htmlmixed.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/htmlmixed/htmlmixed.min.js"></script>
<!-- CodeMirror Addons for VS Code-like experience -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/closetag.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/closebrackets.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/matchbrackets.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/edit/matchtags.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/search/searchcursor.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/search/search.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/dialog/dialog.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/keymap/sublime.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/fold/foldcode.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/fold/foldgutter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/fold/xml-fold.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/fold/brace-fold.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var textArea = document.getElementById('snippetContent'); var textArea = document.getElementById('snippetContent');
@@ -653,6 +685,7 @@
toggleBtn.addEventListener('click', function() { toggleBtn.addEventListener('click', function() {
if (isVisual) { if (isVisual) {
localStorage.setItem('snippetEditorMode', 'code');
var rawOutput; var rawOutput;
if (isGrapesJsModified) { if (isGrapesJsModified) {
@@ -683,9 +716,18 @@
theme: "monokai", theme: "monokai",
lineNumbers: true, lineNumbers: true,
matchBrackets: true, matchBrackets: true,
matchTags: {bothTags: true},
autoCloseTags: true, autoCloseTags: true,
autoCloseBrackets: true,
indentUnit: 4, indentUnit: 4,
lineWrapping: true lineWrapping: true,
keyMap: "sublime",
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"Ctrl-Space": "autocomplete",
"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }
}
}); });
// Fix for initially hidden textarea // Fix for initially hidden textarea
setTimeout(function() { setTimeout(function() {
@@ -705,6 +747,7 @@
toggleBtn.innerHTML = '<i class="fas fa-paint-brush"></i> Use Visual Builder'; toggleBtn.innerHTML = '<i class="fas fa-paint-brush"></i> Use Visual Builder';
} else { } else {
localStorage.setItem('snippetEditorMode', 'visual');
// Get value from CodeMirror back to textarea and GrapesJS // Get value from CodeMirror back to textarea and GrapesJS
if (cmEditor) { if (cmEditor) {
textArea.value = cmEditor.getValue(); textArea.value = cmEditor.getValue();
@@ -737,6 +780,11 @@
} }
} }
}); });
// Restore previous mode if it was Code Editor
if (localStorage.getItem('snippetEditorMode') === 'code') {
toggleBtn.click();
}
}); });
</script> </script>
</section> </section>