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."); redirectAttributes.addFlashAttribute("successMessage", "Snippet deleted successfully.");
return "redirect:/manage/snippets"; 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) --> <!-- 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> <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 --> <!-- Visual Builder Container -->
<div id="gjs-editor-wrap"> <div id="gjs-editor-wrap">
<div class="panel__top"> <div class="panel__top">
@@ -220,29 +223,8 @@
<!-- JS-Beautify for formatting HTML --> <!-- JS-Beautify for formatting HTML -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.7/beautify-html.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.7/beautify-html.min.js"></script>
<!-- CodeMirror JS for syntax highlighting --> <!-- Monaco Editor JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.41.0/min/vs/loader.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>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
@@ -675,8 +657,12 @@
editor.setComponents(existingContent); editor.setComponents(existingContent);
} }
var cmEditor = null; var monacoEditor = null;
var isGrapesJsModified = false; 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 // Track if user made visual changes
editor.on('change:changesCount', function() { editor.on('change:changesCount', function() {
@@ -689,7 +675,6 @@
var rawOutput; var rawOutput;
if (isGrapesJsModified) { if (isGrapesJsModified) {
// User made visual changes, pull from GrapesJS
var html = editor.getHtml(); var html = editor.getHtml();
var css = editor.getCss(); var css = editor.getCss();
var combined = html + (css && css.trim() !== '' && css.trim() !== '* { box-sizing: border-box; } body {margin: 0;}' ? '<style>' + css + '</style>' : ''); 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 preserve_newlines: true
}); });
} else { } else {
// User didn't make visual changes, KEEP IT RAW exactly as it was
rawOutput = textArea.value; rawOutput = textArea.value;
} }
textArea.value = rawOutput; textArea.value = rawOutput;
gjsWrap.style.display = 'none'; gjsWrap.style.display = 'none';
// We don't display textArea because CodeMirror handles the UI
textArea.style.display = 'none'; textArea.style.display = 'none';
monacoContainer.style.display = 'block';
// Initialize CodeMirror if not already if (!monacoEditor) {
if (!cmEditor) { require(['vs/editor/editor.main'], function() {
cmEditor = CodeMirror.fromTextArea(textArea, { monacoEditor = monaco.editor.create(monacoContainer, {
mode: "htmlmixed", value: rawOutput,
theme: "monokai", language: 'html',
lineNumbers: true, theme: 'vs-dark',
matchBrackets: true, automaticLayout: true,
matchTags: {bothTags: true}, formatOnPaste: true,
autoCloseTags: true, formatOnType: 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()); }
}
}); });
// Fix for initially hidden textarea
setTimeout(function() {
cmEditor.refresh();
}, 10);
} else { } else {
cmEditor.setValue(rawOutput); monacoEditor.setValue(rawOutput);
setTimeout(function() {
cmEditor.refresh();
}, 10);
}
// CodeMirror wrapper needs to be visible
if(cmEditor.getWrapperElement()) {
cmEditor.getWrapperElement().style.display = 'block';
} }
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'); localStorage.setItem('snippetEditorMode', 'visual');
// Get value from CodeMirror back to textarea and GrapesJS if (monacoEditor) {
if (cmEditor) { textArea.value = monacoEditor.getValue();
textArea.value = cmEditor.getValue();
cmEditor.getWrapperElement().style.display = 'none';
} }
monacoContainer.style.display = 'none';
editor.setComponents(textArea.value); editor.setComponents(textArea.value);
isGrapesJsModified = false; // Reset flag since we just synced isGrapesJsModified = false;
textArea.style.display = 'none'; textArea.style.display = 'none';
gjsWrap.style.display = 'block'; gjsWrap.style.display = 'block';
@@ -775,8 +738,8 @@
textArea.value = finalOutput; textArea.value = finalOutput;
} }
} else { } else {
if (cmEditor) { if (monacoEditor) {
textArea.value = cmEditor.getValue(); 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"> <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> <i class="fas fa-edit"></i>
</a> </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?');"> <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}" /> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete"> <button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">