Browse Source

Editor: sync panes

imwald
Nuša Pukšič 3 weeks ago
parent
commit
a2fa3537dc
  1. 58
      assets/controllers/editor/json-panel_controller.js
  2. 33
      assets/controllers/editor/layout_controller.js
  3. 29
      assets/controllers/editor/markdown-sync_controller.js
  4. 5
      assets/controllers/nostr/nostr_publish_controller.js
  5. 34
      assets/controllers/publishing/quill_controller.js
  6. 12
      templates/editor/layout.html.twig

58
assets/controllers/editor/json-panel_controller.js

@ -7,10 +7,66 @@ export default class extends Controller { @@ -7,10 +7,66 @@ export default class extends Controller {
console.log('JSON panel controller connected');
this.isDirty = false;
// Listen for the custom event from the Nostr publish controller
document.addEventListener('nostr-json-ready', this.handleNostrJsonReady.bind(this));
// Listen for changes in the markdown textarea
const md = this.getMarkdownTextarea();
if (md) {
md.addEventListener('input', this.handleMarkdownInput.bind(this));
}
// Load initial JSON from the Nostr publish controller
this.loadInitialJson();
}
disconnect() {
// Clean up event listener
document.removeEventListener('nostr-json-ready', this.handleNostrJsonReady.bind(this));
const md = this.getMarkdownTextarea();
if (md) {
md.removeEventListener('input', this.handleMarkdownInput.bind(this));
}
}
handleMarkdownInput() {
// When markdown changes, update the JSON content field and panel
this.updateJsonContentFromMarkdown();
}
updateJsonContentFromMarkdown() {
if (!this.hasJsonTextareaTarget) return;
let json;
try {
json = JSON.parse(this.jsonTextareaTarget.value);
} catch (e) {
return; // Don't update if JSON is invalid
}
const md = this.getMarkdownTextarea();
if (md) {
json.content = md.value;
this.jsonTextareaTarget.value = JSON.stringify(json, null, 2);
this.formatJson();
}
}
getMarkdownTextarea() {
// Try common selectors for the markdown textarea
return document.querySelector('#editor_content, textarea[name="editor[content]"]');
}
handleNostrJsonReady(event) {
const nostrController = this.getNostrPublishController();
if (nostrController && nostrController.hasJsonTextareaTarget && this.hasJsonTextareaTarget) {
this.jsonTextareaTarget.value = nostrController.jsonTextareaTarget.value;
this.updateJsonContentFromMarkdown();
this.formatJson();
this.isDirty = false;
this.updateDirtyHint();
this.showStatus('JSON updated', 'success');
}
}
loadInitialJson() {
// Wait a bit for the Nostr publish controller to initialize
setTimeout(() => {
@ -19,6 +75,7 @@ export default class extends Controller { @@ -19,6 +75,7 @@ export default class extends Controller {
const json = nostrController.jsonTextareaTarget.value;
if (json && this.hasJsonTextareaTarget) {
this.jsonTextareaTarget.value = json;
this.updateJsonContentFromMarkdown();
this.formatJson();
}
}
@ -115,4 +172,3 @@ export default class extends Controller { @@ -115,4 +172,3 @@ export default class extends Controller {
);
}
}

33
assets/controllers/editor/layout_controller.js

@ -41,6 +41,18 @@ export default class extends Controller { @@ -41,6 +41,18 @@ export default class extends Controller {
}
}
}
// Listen for content changes from Quill or Markdown
this.element.addEventListener('content:changed', () => {
this.updatePreview();
// If JSON pane is present, update it as well
if (this.hasJsonPaneTarget) {
const jsonTextarea = this.jsonPaneTarget.querySelector('[data-editor--json-panel-target="jsonTextarea"]');
if (jsonTextarea && window.nostrPublishController && typeof window.nostrPublishController.regenerateJsonPreview === 'function') {
window.nostrPublishController.regenerateJsonPreview();
}
}
});
}
switchMode(event) {
@ -60,6 +72,23 @@ export default class extends Controller { @@ -60,6 +72,23 @@ export default class extends Controller {
// Update content when switching modes
if (mode === 'markdown') {
this.updateMarkdown();
} else if (mode === 'edit') {
// Sync Markdown to Quill when switching to Quill pane
const markdownInput = this.element.querySelector('input[name="editor[content]"]');
if (markdownInput && window.appQuill) {
if (window.marked) {
window.appQuill.root.innerHTML = window.marked.parse(markdownInput.value || '');
} else {
// Fallback: use backend endpoint
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 || ''; });
}
}
} else if (mode === 'preview') {
this.updatePreview();
} else if (mode === 'json') {
@ -75,7 +104,7 @@ export default class extends Controller { @@ -75,7 +104,7 @@ export default class extends Controller {
}
// Get markdown from Quill controller
const markdownInput = this.element.querySelector('input[name="editor[content_md]"]');
const markdownInput = this.element.querySelector('input[name="editor[content]"]');
const markdown = markdownInput ? markdownInput.value || '' : '';
// Set code block content and highlight
@ -94,7 +123,7 @@ export default class extends Controller { @@ -94,7 +123,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_md]"]');
const markdownInput = this.element.querySelector('input[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]"]');

29
assets/controllers/editor/markdown-sync_controller.js

@ -6,6 +6,10 @@ export default class extends Controller { @@ -6,6 +6,10 @@ export default class extends Controller {
connect() {
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"] });
@ -16,8 +20,29 @@ export default class extends Controller { @@ -16,8 +20,29 @@ export default class extends Controller {
if (this.observer) this.observer.disconnect();
}
updateMarkdown() {
async updateMarkdown() {
this.codeTarget.textContent = this.hiddenTarget.value;
// Sync Markdown to Quill (content_html)
if (window.appQuill) {
let html = '';
if (window.marked) {
html = window.marked.parse(this.hiddenTarget.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 || '' })
});
if (resp.ok) {
const data = await resp.json();
html = data.html || '';
}
} catch (e) { html = ''; }
}
// Set Quill content from HTML (replace contents)
window.appQuill.root.innerHTML = html;
}
}
}

5
assets/controllers/nostr/nostr_publish_controller.js

@ -125,6 +125,8 @@ export default class extends Controller { @@ -125,6 +125,8 @@ export default class extends Controller {
if (this.hasJsonTextareaTarget) this.jsonTextareaTarget.value = pretty;
this.jsonEdited = false;
if (this.hasJsonDirtyHintTarget) this.jsonDirtyHintTarget.style.display = 'none';
// Dispatch event to notify others that JSON is ready
this.element.dispatchEvent(new CustomEvent('nostr-json-ready', { bubbles: true }));
} catch (e) {
this.showError('Could not build event JSON: ' + (e?.message || e));
}
@ -270,7 +272,7 @@ export default class extends Controller { @@ -270,7 +272,7 @@ export default class extends Controller {
const fd = new FormData(form);
// Prefer the Markdown field populated by the Quill controller
const md = fd.get('editor[content_md]');
const md = fd.get('editor[content]');
let html = fd.get('editor[content]') || fd.get('content') || '';
// Final content: use MD if present, otherwise convert HTML -> MD
@ -530,4 +532,3 @@ export default class extends Controller { @@ -530,4 +532,3 @@ export default class extends Controller {
}
}
}

34
assets/controllers/publishing/quill_controller.js

@ -181,26 +181,28 @@ export default class extends Controller { @@ -181,26 +181,28 @@ export default class extends Controller {
this.quill.on('text-change', (delta, old, source) => {
if (source === 'user') highlightAll();
this.syncHiddenAsHtml();
// --- Quill → Markdown sync ---
if (this.hasMarkdownTarget) {
if (window.deltaToMarkdown) {
const md = window.deltaToMarkdown(this.quill.getContents());
this.markdownTarget.value = md;
// Trigger event for reactivity
this.markdownTarget.dispatchEvent(new Event('input', { bubbles: true }));
// Also trigger a custom event for layout controller
this.element.dispatchEvent(new CustomEvent('content:changed', { bubbles: true }));
}
}
});
const sync = () => {
// HTML
if (this.hasHiddenTarget) this.hiddenTarget.value = this.quill.root.innerHTML;
// Markdown (from Delta)
if (this.hasMarkdownTarget) this.markdownTarget.value = deltaToMarkdown(this.quill.getContents());
// Expose a method to set Quill content from HTML
window.setQuillHtml = (html) => {
this.quill.root.innerHTML = html;
};
// sync on load and on every edit
sync();
this.quill.on('text-change', (delta, oldDelta, source) => {
if (source === 'user') highlightAll();
sync();
});
// safety: also refresh MD/HTML right before a real submit (if any)
// safety: also refresh HTML right before a real submit (if any)
const form = this.element.closest('form');
if (form) {
form.addEventListener('submit', () => sync());
form.addEventListener('submit', () => this.syncHiddenAsHtml());
}
}
@ -454,3 +456,7 @@ function deltaToMarkdown(delta) { @@ -454,3 +456,7 @@ function deltaToMarkdown(delta) {
out = out.replace(/\n{3,}/g, '\n\n');
return out.trim();
}
// Make deltaToMarkdown globally available
window.deltaToMarkdown = deltaToMarkdown;

12
templates/editor/layout.html.twig

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
<div id="editor">
{{ value|raw }}
</div>
<input type="hidden" name="editor[content_md]" data-publishing--quill-target="markdown" data-editor--markdown-sync-target="hidden">
<input type="hidden" name="editor[content]" data-publishing--quill-target="markdown" data-editor--markdown-sync-target="hidden">
<input type="hidden" {{ block('widget_attributes') }} value="{{ value }}" />
</div>
{% endblock %}
@ -186,8 +186,8 @@ @@ -186,8 +186,8 @@
}) }}
</div>
{# QuillJS editor container #}
{{ form_row(form.content, {'label': false}) }}
{# QuillJS editor container, now using content_html #}
{{ form_row(form.content_html, {'label': false}) }}
{# Hidden field for draft status - controlled by Save Draft / Publish buttons #}
<div style="display: none;">
@ -210,7 +210,7 @@ @@ -210,7 +210,7 @@
data-editor--layout-target="markdownPane"
data-controller="editor--markdown-sync"
>
{# Markdown editor #}
{# Markdown editor - source of truth #}
<div class="markdown-editor-wrapper">
<div class="editor-title-input">
<input
@ -221,7 +221,9 @@ @@ -221,7 +221,9 @@
readonly
/>
</div>
<pre class="markdown-highlight"><code class="language-markdown" data-editor--layout-target="markdownCode" data-editor--markdown-sync-target="code"></code></pre>
{{ form_row(form.content, {'label': false, 'attr': {'class': 'form-control editor-md-field', 'data-editor--markdown-sync-target': 'hidden'}}) }}
<pre class="mt-2" data-editor--markdown-sync-target="code" style="display:none"></pre>
{# JS should sync changes in content to content_html (Quill) #}
</div>
</div>

Loading…
Cancel
Save