tích hợp monaco-editor
This commit is contained in:
+16
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user