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.
 
 
 
 
 
 

207 lines
7.3 KiB

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'];
connect() {
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();
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() {
// 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));
}
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() {
// 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(() => {
const nostrController = this.getNostrPublishController();
if (nostrController && nostrController.hasJsonTextareaTarget) {
const json = nostrController.jsonTextareaTarget.value;
if (json && this.hasJsonTextareaTarget) {
this.jsonTextareaTarget.value = json;
this.updateJsonContentFromMarkdown();
this.formatJson();
}
}
}, 500);
}
regenerateJson() {
const nostrController = this.getNostrPublishController();
if (nostrController && typeof nostrController.regenerateJsonPreview === 'function') {
nostrController.regenerateJsonPreview();
// Copy the regenerated JSON to our textarea
setTimeout(() => {
if (nostrController.hasJsonTextareaTarget && this.hasJsonTextareaTarget) {
this.jsonTextareaTarget.value = nostrController.jsonTextareaTarget.value;
this.formatJson();
this.isDirty = false;
this.updateDirtyHint();
this.showStatus('JSON rebuilt from form', 'success');
}
}, 100);
}
}
onJsonInput(event) {
this.isDirty = true;
this.updateDirtyHint();
this.validateJson();
// Sync to the hidden Nostr publish textarea
const nostrController = this.getNostrPublishController();
if (nostrController && nostrController.hasJsonTextareaTarget) {
nostrController.jsonTextareaTarget.value = event.target.value;
}
}
validateJson() {
if (!this.hasJsonTextareaTarget) return;
try {
const json = JSON.parse(this.jsonTextareaTarget.value);
const required = ['kind', 'created_at', 'tags', 'content', 'pubkey'];
const missing = required.filter(field => !(field in json));
if (missing.length > 0) {
this.showStatus(`Missing: ${missing.join(', ')}`, 'warning');
} else {
this.showStatus('Valid JSON', 'success');
}
} catch (e) {
this.showStatus('Invalid JSON', 'error');
}
}
formatJson() {
if (!this.hasJsonTextareaTarget) return;
try {
const json = JSON.parse(this.jsonTextareaTarget.value);
this.jsonTextareaTarget.value = JSON.stringify(json, null, 2);
this.showStatus('Formatted', 'success');
} catch (e) {
// Silently fail if JSON is invalid
}
}
showStatus(message, type = 'info') {
if (!this.hasStatusTarget) return;
this.statusTarget.textContent = message;
this.statusTarget.className = `json-status json-status--${type}`;
setTimeout(() => {
if (this.hasStatusTarget) {
this.statusTarget.textContent = '';
this.statusTarget.className = 'json-status';
}
}, 3000);
}
updateDirtyHint() {
if (this.hasDirtyHintTarget) {
this.dirtyHintTarget.style.display = this.isDirty ? 'inline' : 'none';
}
}
getNostrPublishController() {
const element = document.querySelector('[data-controller*="nostr--nostr-publish"]');
if (!element) return null;
return this.application.getControllerForElementAndIdentifier(
element,
'nostr--nostr-publish'
);
}
}