clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

275 lines
12 KiB

// assets/controllers/editor/layout_controller.js
import {Controller} from '@hotwired/stimulus';
export default class extends Controller {
static targets = [
'modeTab', 'editPane', 'markdownPane', 'jsonPane', 'previewPane',
'previewBody', 'previewTitle',
'previewSummary', 'previewImage', 'previewImagePlaceholder', 'previewAuthor', 'previewDate',
'markdownEditor', 'markdownTitle', 'markdownCode', 'status'
];
connect() {
console.log('Editor layout controller connected');
this.autoSaveTimer = null;
// Live preview for summary and image fields
const summaryInput = this.element.querySelector('textarea[name*="[summary]"], textarea[name="editor[summary]"]');
const imageInput = this.element.querySelector('input[name*="[image]"], input[name="editor[image]"]');
if (summaryInput) {
summaryInput.addEventListener('input', () => this.updatePreview());
summaryInput.addEventListener('change', () => this.updatePreview());
}
if (imageInput) {
imageInput.addEventListener('input', () => this.updatePreview());
imageInput.addEventListener('change', () => this.updatePreview());
}
// If editing an existing article, load JSON event by default
if (this.element.dataset.articleId && this.hasJsonPaneTarget) {
// Find the JSON textarea in the pane and load the event
const jsonTextarea = this.jsonPaneTarget.querySelector('[data-editor--json-panel-target="jsonTextarea"]');
if (jsonTextarea && !jsonTextarea.value.trim()) {
// Try to get the Nostr publish controller's JSON
const nostrController = this.application.getControllerForElementAndIdentifier(
this.element.querySelector('[data-controller*="nostr--nostr-publish"]'),
'nostr--nostr-publish'
);
if (nostrController && nostrController.hasJsonTextareaTarget) {
jsonTextarea.value = nostrController.jsonTextareaTarget.value;
// Optionally, trigger formatting/validation if needed
}
}
}
// 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"]');
if (jsonTextarea && window.nostrPublishController && typeof window.nostrPublishController.regenerateJsonPreview === 'function') {
window.nostrPublishController.regenerateJsonPreview();
}
}
});
}
switchMode(event) {
const mode = event.currentTarget.dataset.mode;
// Update tab states
this.modeTabTargets.forEach(tab => {
tab.classList.toggle('is-active', tab.dataset.mode === mode);
});
// Toggle panes - hide all, then show the selected one
this.editPaneTarget.classList.toggle('is-hidden', mode !== 'edit');
this.markdownPaneTarget.classList.toggle('is-hidden', mode !== 'markdown');
this.jsonPaneTarget.classList.toggle('is-hidden', mode !== 'json');
this.previewPaneTarget.classList.toggle('is-hidden', mode !== 'preview');
// 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('textarea[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') {
// Optionally, trigger JSON formatting/validation
}
}
updateMarkdown() {
// Get title from form
const titleInput = this.element.querySelector('input[name*="[title]"]');
if (titleInput && this.hasMarkdownTitleTarget) {
this.markdownTitleTarget.value = titleInput.value || '';
}
// Get markdown from Quill controller
const markdownInput = this.element.querySelector('textarea[name="editor[content]"]');
const markdown = markdownInput ? markdownInput.value || '' : '';
// Set code block content and highlight
if (this.hasMarkdownCodeTarget) {
this.markdownCodeTarget.textContent = markdown;
if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.markdownCodeTarget);
}
}
}
async updatePreview() {
if (!this.hasPreviewBodyTarget) return;
// Get title from form
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('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]"]');
// Title
const title = titleInput ? titleInput.value.trim() : '';
if (this.hasPreviewTitleTarget) {
this.previewTitleTarget.textContent = title || 'Article title';
}
// Author (placeholder logic)
if (this.hasPreviewAuthorTarget) {
let author = authorInput ? authorInput.value.trim() : '';
this.previewAuthorTarget.textContent = author || 'Author';
}
// Date (placeholder logic)
if (this.hasPreviewDateTarget) {
const now = new Date();
this.previewDateTarget.textContent = now.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// Summary (always use form value)
const summary = summaryInput ? summaryInput.value.trim() : '';
if (this.hasPreviewSummaryTarget) {
this.previewSummaryTarget.textContent = summary || 'No summary provided. This is where your article summary will appear.';
this.previewSummaryTarget.classList.toggle('placeholder', !summary);
}
// Image (always use form value)
const imageUrl = imageInput ? imageInput.value.trim() : '';
if (this.hasPreviewImageTarget && this.hasPreviewImagePlaceholderTarget) {
if (imageUrl) {
this.previewImageTarget.src = imageUrl;
this.previewImageTarget.style.display = '';
this.previewImagePlaceholderTarget.style.display = 'none';
} else {
this.previewImageTarget.src = '';
this.previewImageTarget.style.display = 'none';
this.previewImagePlaceholderTarget.style.display = '';
}
}
// Body (markdown to HTML via backend)
let html = '<p><em>Loading preview...</em></p>';
this.previewBodyTarget.innerHTML = html;
if (markdownInput) {
try {
const response = await fetch('/editor/markdown/preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ markdown: markdownInput.value || '' })
});
if (response.ok) {
const data = await response.json();
html = data.html || '<p><em>No content yet. Start writing your article!</em></p>';
} else {
html = '<p><em>Failed to load preview.</em></p>';
}
} catch (e) {
html = '<p><em>Error loading preview.</em></p>';
}
this.previewBodyTarget.innerHTML = html;
} else {
this.previewBodyTarget.innerHTML = '<p><em>No content yet. Start writing your article!</em></p>';
}
}
saveDraft() {
console.log('Saving draft...');
// Mark as draft - set checkbox to true
const draftCheckbox = this.element.querySelector('input[name*="[isDraft]"]');
if (draftCheckbox) {
draftCheckbox.checked = true;
}
// Submit the form
const form = this.element.querySelector('form');
if (form) {
this.updateStatus('Saving draft...');
form.requestSubmit();
}
}
publish() {
console.log('Publishing article...');
// Mark as NOT draft - set checkbox to false
const draftCheckbox = this.element.querySelector('input[name*="[isDraft]"]');
if (draftCheckbox) {
draftCheckbox.checked = false;
}
// Find the Nostr publish controller and trigger publish
const nostrController = this.application.getControllerForElementAndIdentifier(
this.element.querySelector('[data-controller*="nostr--nostr-publish"]'),
'nostr--nostr-publish'
);
if (nostrController) {
nostrController.publish();
} else {
console.error('Nostr publish controller not found');
alert('Could not find publishing controller. Please try again.');
}
}
updateStatus(message) {
if (this.hasStatusTarget) {
this.statusTarget.textContent = message;
// Clear status after 3 seconds
setTimeout(() => {
if (this.hasStatusTarget) {
this.statusTarget.textContent = '';
}
}, 3000);
}
}
disconnect() {
if (this.autoSaveTimer) {
clearTimeout(this.autoSaveTimer);
}
}
}