diff --git a/assets/controllers/editor/json-panel_controller.js b/assets/controllers/editor/json-panel_controller.js index 86c2317..3df50b2 100644 --- a/assets/controllers/editor/json-panel_controller.js +++ b/assets/controllers/editor/json-panel_controller.js @@ -1,4 +1,6 @@ import { Controller } from '@hotwired/stimulus'; +import { EditorView, basicSetup } from 'codemirror'; +import { json } from '@codemirror/lang-json'; export default class extends Controller { static targets = ['jsonTextarea', 'status', 'dirtyHint']; @@ -18,6 +20,31 @@ export default class extends Controller { // Load initial JSON from the Nostr publish controller this.loadInitialJson(); + + this.textarea = this.jsonTextareaTarget; + // Only initialize CodeMirror if not already done + if (!this.textarea._codemirror) { + this.textarea.style.display = 'none'; + this.cmParent = document.createElement('div'); + this.textarea.parentNode.insertBefore(this.cmParent, this.textarea); + this.cmView = new EditorView({ + doc: this.textarea.value, + extensions: [ + basicSetup, json(), + EditorView.lineWrapping, + ], + parent: this.cmParent, + updateListener: (update) => { + if (update.docChanged) { + this.textarea.value = this.cmView.state.doc.toString(); + this.textarea.dispatchEvent(new Event('input', { bubbles: true })); + } + } + }); + this.textarea._codemirror = this.cmView; + } else { + this.cmView = this.textarea._codemirror; + } } disconnect() { @@ -27,6 +54,12 @@ export default class extends Controller { if (md) { md.removeEventListener('input', this.handleMarkdownInput.bind(this)); } + if (this.cmView) this.cmView.destroy(); + if (this.cmParent && this.cmParent.parentNode) { + this.cmParent.parentNode.removeChild(this.cmParent); + } + this.textarea.style.display = ''; + this.textarea._codemirror = null; } handleMarkdownInput() { diff --git a/assets/controllers/editor/layout_controller.js b/assets/controllers/editor/layout_controller.js index 8f7ce2c..22fde58 100644 --- a/assets/controllers/editor/layout_controller.js +++ b/assets/controllers/editor/layout_controller.js @@ -45,6 +45,21 @@ export default class extends Controller { // Listen for content changes from Quill or Markdown this.element.addEventListener('content:changed', () => { this.updatePreview(); + // Update Quill pane live + const markdownInput = this.element.querySelector('textarea[name="editor[content]"]'); + if (markdownInput && window.appQuill) { + if (window.marked) { + window.appQuill.root.innerHTML = window.marked.parse(markdownInput.value || ''); + } else { + fetch('/editor/markdown/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, + body: JSON.stringify({ markdown: markdownInput.value || '' }) + }) + .then(resp => resp.ok ? resp.json() : { html: '' }) + .then(data => { window.appQuill.root.innerHTML = data.html || ''; }); + } + } // If JSON pane is present, update it as well if (this.hasJsonPaneTarget) { const jsonTextarea = this.jsonPaneTarget.querySelector('[data-editor--json-panel-target="jsonTextarea"]'); @@ -74,7 +89,7 @@ export default class extends Controller { this.updateMarkdown(); } else if (mode === 'edit') { // Sync Markdown to Quill when switching to Quill pane - const markdownInput = this.element.querySelector('input[name="editor[content]"]'); + const markdownInput = this.element.querySelector('textarea[name="editor[content]"]'); if (markdownInput && window.appQuill) { if (window.marked) { window.appQuill.root.innerHTML = window.marked.parse(markdownInput.value || ''); @@ -104,7 +119,7 @@ export default class extends Controller { } // Get markdown from Quill controller - const markdownInput = this.element.querySelector('input[name="editor[content]"]'); + const markdownInput = this.element.querySelector('textarea[name="editor[content]"]'); const markdown = markdownInput ? markdownInput.value || '' : ''; // Set code block content and highlight @@ -123,7 +138,7 @@ export default class extends Controller { const titleInput = this.element.querySelector('input[name*="[title]"], input[name="editor[title]"]'); const summaryInput = this.element.querySelector('textarea[name*="[summary]"], textarea[name="editor[summary]"]'); const imageInput = this.element.querySelector('input[name*="[image]"], input[name="editor[image]"]'); - const markdownInput = this.element.querySelector('input[name="editor[content]"]'); + const markdownInput = this.element.querySelector('textarea[name="editor[content]"]'); const authorInput = this.element.querySelector('input[name*="[author]"]'); const dateInput = this.element.querySelector('input[name*="[publishedAt]"]') || this.element.querySelector('input[name*="[createdAt]"]'); diff --git a/assets/controllers/editor/markdown-sync_controller.js b/assets/controllers/editor/markdown-sync_controller.js index 4b1c462..f242dfb 100644 --- a/assets/controllers/editor/markdown-sync_controller.js +++ b/assets/controllers/editor/markdown-sync_controller.js @@ -1,39 +1,68 @@ import { Controller } from "@hotwired/stimulus"; +import { EditorView, basicSetup } from "codemirror"; +import { markdown } from "@codemirror/lang-markdown"; export default class extends Controller { - static targets = ["hidden", "code"]; - connect() { + this.textarea = this.element.querySelector(".editor-md-field"); + this.codePreview = this.element.querySelector("pre"); + // Only initialize CodeMirror if not already done + if (!this.textarea._codemirror) { + this.textarea.style.display = "none"; + this.cmParent = document.createElement("div"); + this.textarea.parentNode.insertBefore(this.cmParent, this.textarea); + this.cmView = new EditorView({ + doc: this.textarea.value, + extensions: [ + basicSetup, + markdown(), + EditorView.lineWrapping, + EditorView.updateListener.of((update) => { + if (update.docChanged) { + this.textarea.value = update.state.doc.toString(); + this.updateMarkdown(); + this.element.dispatchEvent(new CustomEvent('content:changed', { bubbles: true })); + } + }) + ], + parent: this.cmParent + }); + this.textarea._codemirror = this.cmView; + } else { + this.cmView = this.textarea._codemirror; + } this.updateMarkdown(); - this.hiddenTarget.addEventListener("input", this.updateMarkdown.bind(this)); - // Also trigger a custom event for layout controller - this.hiddenTarget.addEventListener("input", () => { - this.element.dispatchEvent(new CustomEvent('content:changed', { bubbles: true })); - }); // Observe programmatic changes to the value attribute this.observer = new MutationObserver(() => this.updateMarkdown()); - this.observer.observe(this.hiddenTarget, { attributes: true, attributeFilter: ["value"] }); + this.observer.observe(this.textarea, { attributes: true, attributeFilter: ["value"] }); } disconnect() { - this.hiddenTarget.removeEventListener("input", this.updateMarkdown.bind(this)); if (this.observer) this.observer.disconnect(); + if (this.cmView) this.cmView.destroy(); + if (this.cmParent && this.cmParent.parentNode) { + this.cmParent.parentNode.removeChild(this.cmParent); + } + this.textarea.style.display = ""; + this.textarea._codemirror = null; } async updateMarkdown() { - this.codeTarget.textContent = this.hiddenTarget.value; + if (this.codePreview) { + this.codePreview.textContent = this.textarea.value; + } // Sync Markdown to Quill (content_html) if (window.appQuill) { let html = ''; if (window.marked) { - html = window.marked.parse(this.hiddenTarget.value || ''); + html = window.marked.parse(this.textarea.value || ''); } else { // Fallback: use backend endpoint try { const resp = await fetch('/editor/markdown/preview', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, - body: JSON.stringify({ markdown: this.hiddenTarget.value || '' }) + body: JSON.stringify({ markdown: this.textarea.value || '' }) }); if (resp.ok) { const data = await resp.json(); diff --git a/assets/styles/editor-layout.css b/assets/styles/editor-layout.css index c7526d2..19aad13 100644 --- a/assets/styles/editor-layout.css +++ b/assets/styles/editor-layout.css @@ -109,6 +109,12 @@ main[data-controller="editor--layout"] { display: flex; min-height: 0; margin: 0; + max-width: 100%; +} + +.cm-editor { + height: 100%; + max-width: 100%; } .editor-pane { @@ -221,7 +227,7 @@ main[data-controller="editor--layout"] { padding: 0; margin: 0 !important; overflow-y: auto; - background-color: var(--color-bg-light); + background-color: var(--color-bg); } .preview-container { diff --git a/importmap.php b/importmap.php index 573fbc7..d3e4ef9 100644 --- a/importmap.php +++ b/importmap.php @@ -164,4 +164,82 @@ return [ 'version' => '1.30.0', 'type' => 'css', ], + 'codemirror' => [ + 'version' => '6.0.2', + ], + '@codemirror/lang-markdown' => [ + 'version' => '6.5.0', + ], + '@codemirror/theme-one-dark' => [ + 'version' => '6.1.3', + ], + '@codemirror/view' => [ + 'version' => '6.37.2', + ], + '@codemirror/state' => [ + 'version' => '6.5.2', + ], + '@codemirror/language' => [ + 'version' => '6.11.1', + ], + '@codemirror/commands' => [ + 'version' => '6.8.1', + ], + '@codemirror/search' => [ + 'version' => '6.5.11', + ], + '@codemirror/autocomplete' => [ + 'version' => '6.18.6', + ], + '@codemirror/lint' => [ + 'version' => '6.8.5', + ], + '@lezer/markdown' => [ + 'version' => '1.5.1', + ], + '@codemirror/lang-html' => [ + 'version' => '6.4.11', + ], + '@lezer/common' => [ + 'version' => '1.2.3', + ], + '@lezer/highlight' => [ + 'version' => '1.2.1', + ], + 'style-mod' => [ + 'version' => '4.1.2', + ], + 'w3c-keyname' => [ + 'version' => '2.2.8', + ], + 'crelt' => [ + 'version' => '1.0.6', + ], + '@marijn/find-cluster-break' => [ + 'version' => '1.0.2', + ], + '@lezer/html' => [ + 'version' => '1.3.12', + ], + '@codemirror/lang-css' => [ + 'version' => '6.3.1', + ], + '@codemirror/lang-javascript' => [ + 'version' => '6.2.4', + ], + '@lezer/lr' => [ + 'version' => '1.4.2', + ], + '@lezer/css' => [ + 'version' => '1.1.9', + ], + '@lezer/javascript' => [ + 'version' => '1.5.1', + ], + '@codemirror/lang-json' => [ + 'version' => '6.0.2', + ], + '@lezer/json' => [ + 'version' => '1.0.3', + ], ]; diff --git a/templates/editor/layout.html.twig b/templates/editor/layout.html.twig index 8fb9ba0..d9bbfa2 100644 --- a/templates/editor/layout.html.twig +++ b/templates/editor/layout.html.twig @@ -8,7 +8,6 @@