tích hợp monaco-editor

This commit is contained in:
2026-07-13 16:17:19 +07:00
parent af82bfc481
commit 157d7eba7b
3 changed files with 51 additions and 66 deletions
@@ -63,4 +63,20 @@ public class ManageSnippetController {
redirectAttributes.addFlashAttribute("successMessage", "Snippet deleted successfully.");
return "redirect:/manage/snippets";
}
@PostMapping("/{id}/duplicate")
public String duplicateSnippet(@PathVariable Long id, RedirectAttributes redirectAttributes) {
return snippetService.findById(id).map(snippet -> {
HtmlSnippet duplicated = new HtmlSnippet();
duplicated.setName(snippet.getName() + " (Copy)");
duplicated.setSlug(snippet.getSlug() + "-copy-" + System.currentTimeMillis());
duplicated.setContent(snippet.getContent());
duplicated.setActive(false);
HtmlSnippet saved = snippetService.save(duplicated);
redirectAttributes.addFlashAttribute("successMessage", "Snippet duplicated successfully.");
return "redirect:/manage/snippets/" + saved.getId() + "/edit";
}).orElseGet(() -> {
redirectAttributes.addFlashAttribute("errorMessage", "Snippet not found.");
return "redirect:/manage/snippets";
});
}
}
@@ -158,6 +158,9 @@
<!-- Raw Textarea (hidden by default) -->
<textarea class="form-control" id="snippetContent" name="content" th:text="*{content}" rows="20" style="font-family: monospace; display: none;"></textarea>
<!-- Monaco Editor Container -->
<div id="monacoEditorContainer" style="width: 100%; height: 75vh; min-height: 550px; border: 1px solid #d1d3e2; border-radius: 0.35rem; display: none;"></div>
<!-- Visual Builder Container -->
<div id="gjs-editor-wrap">
<div class="panel__top">
@@ -220,29 +223,8 @@
<!-- JS-Beautify for formatting HTML -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.7/beautify-html.min.js"></script>
<!-- CodeMirror JS for syntax highlighting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/xml/xml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/css/css.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>
<!-- 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>
<!-- Monaco Editor JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.41.0/min/vs/loader.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
@@ -675,9 +657,13 @@
editor.setComponents(existingContent);
}
var cmEditor = null;
var monacoEditor = null;
var isGrapesJsModified = false;
var monacoContainer = document.getElementById('monacoEditorContainer');
// Configure Monaco
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.41.0/min/vs' }});
// Track if user made visual changes
editor.on('change:changesCount', function() {
isGrapesJsModified = true;
@@ -689,7 +675,6 @@
var rawOutput;
if (isGrapesJsModified) {
// User made visual changes, pull from GrapesJS
var html = editor.getHtml();
var css = editor.getCss();
var combined = html + (css && css.trim() !== '' && css.trim() !== '* { box-sizing: border-box; } body {margin: 0;}' ? '<style>' + css + '</style>' : '');
@@ -700,61 +685,39 @@
preserve_newlines: true
});
} else {
// User didn't make visual changes, KEEP IT RAW exactly as it was
rawOutput = textArea.value;
}
textArea.value = rawOutput;
gjsWrap.style.display = 'none';
// We don't display textArea because CodeMirror handles the UI
textArea.style.display = 'none';
monacoContainer.style.display = 'block';
// Initialize CodeMirror if not already
if (!cmEditor) {
cmEditor = CodeMirror.fromTextArea(textArea, {
mode: "htmlmixed",
theme: "monokai",
lineNumbers: true,
matchBrackets: true,
matchTags: {bothTags: true},
autoCloseTags: true,
autoCloseBrackets: true,
indentUnit: 4,
lineWrapping: true,
keyMap: "sublime",
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"Ctrl-Space": "autocomplete",
"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }
}
if (!monacoEditor) {
require(['vs/editor/editor.main'], function() {
monacoEditor = monaco.editor.create(monacoContainer, {
value: rawOutput,
language: 'html',
theme: 'vs-dark',
automaticLayout: true,
formatOnPaste: true,
formatOnType: true
});
});
// Fix for initially hidden textarea
setTimeout(function() {
cmEditor.refresh();
}, 10);
} else {
cmEditor.setValue(rawOutput);
setTimeout(function() {
cmEditor.refresh();
}, 10);
}
// CodeMirror wrapper needs to be visible
if(cmEditor.getWrapperElement()) {
cmEditor.getWrapperElement().style.display = 'block';
monacoEditor.setValue(rawOutput);
}
toggleBtn.innerHTML = '<i class="fas fa-paint-brush"></i> Use Visual Builder';
} else {
localStorage.setItem('snippetEditorMode', 'visual');
// Get value from CodeMirror back to textarea and GrapesJS
if (cmEditor) {
textArea.value = cmEditor.getValue();
cmEditor.getWrapperElement().style.display = 'none';
if (monacoEditor) {
textArea.value = monacoEditor.getValue();
}
monacoContainer.style.display = 'none';
editor.setComponents(textArea.value);
isGrapesJsModified = false; // Reset flag since we just synced
isGrapesJsModified = false;
textArea.style.display = 'none';
gjsWrap.style.display = 'block';
@@ -775,8 +738,8 @@
textArea.value = finalOutput;
}
} else {
if (cmEditor) {
textArea.value = cmEditor.getValue();
if (monacoEditor) {
textArea.value = monacoEditor.getValue();
}
}
});
@@ -54,6 +54,12 @@
<a th:href="@{/manage/snippets/{id}/edit(id=${snippet.id})}" class="btn btn-sm btn-outline-info mr-1" title="Edit">
<i class="fas fa-edit"></i>
</a>
<form th:action="@{/manage/snippets/{id}/duplicate(id=${snippet.id})}" method="post" style="display:inline;">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button type="submit" class="btn btn-sm btn-outline-warning mr-1" title="Duplicate">
<i class="fas fa-copy"></i>
</button>
</form>
<form th:action="@{/manage/snippets/{id}/delete(id=${snippet.id})}" method="post" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this snippet?');">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">