7 changed files with 139 additions and 399 deletions
@ -1,284 +0,0 @@
@@ -1,284 +0,0 @@
|
||||
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; |
||||
} |
||||
|
||||
// Prompt to restore from localStorage if available and textarea is empty
|
||||
if (this.jsonTextareaTarget.value.trim() === '') { |
||||
const savedJson = localStorage.getItem('editorState'); |
||||
if (savedJson) { |
||||
let shouldRestore = window.confirm('A draft was found in your browser. Do you want to restore it?'); |
||||
if (shouldRestore) { |
||||
try { |
||||
const parsedJson = JSON.parse(savedJson); // Validate JSON
|
||||
this.jsonTextareaTarget.value = savedJson; |
||||
if (this.cmView) { |
||||
this.cmView.dispatch({ |
||||
changes: {from: 0, to: this.cmView.state.doc.length, insert: savedJson} |
||||
}); |
||||
} |
||||
this.populateFormFieldsFromJson(parsedJson); |
||||
} catch (e) { |
||||
// Ignore corrupt JSON
|
||||
localStorage.removeItem('editorState'); |
||||
} |
||||
} else { |
||||
localStorage.removeItem('editorState'); |
||||
} |
||||
} |
||||
} |
||||
// Periodic save every 10 seconds
|
||||
this._saveInterval = setInterval(() => { |
||||
if (this.hasJsonTextareaTarget) { |
||||
const value = this.jsonTextareaTarget.value; |
||||
try { |
||||
JSON.parse(value); // Only save valid JSON
|
||||
localStorage.setItem('editorState', value); |
||||
} catch (e) { |
||||
// Do not save invalid JSON
|
||||
} |
||||
} |
||||
}, 10000); |
||||
} |
||||
|
||||
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; |
||||
// Update CodeMirror document to match textarea
|
||||
if (this.cmView) { |
||||
this.cmView.dispatch({ |
||||
changes: {from: 0, to: this.cmView.state.doc.length, insert: this.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; |
||||
// Update CodeMirror document to match textarea
|
||||
if (this.cmView) { |
||||
this.cmView.dispatch({ |
||||
changes: {from: 0, to: this.cmView.state.doc.length, insert: this.jsonTextareaTarget.value} |
||||
}); |
||||
} |
||||
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; |
||||
// Update CodeMirror document to match textarea
|
||||
if (this.cmView) { |
||||
this.cmView.dispatch({ |
||||
changes: {from: 0, to: this.cmView.state.doc.length, insert: this.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' |
||||
); |
||||
} |
||||
|
||||
// Populate form fields (markdown, title, etc.) from JSON
|
||||
populateFormFieldsFromJson(json) { |
||||
// Markdown content
|
||||
const md = this.getMarkdownTextarea(); |
||||
if (md && json.content !== undefined) { |
||||
md.value = json.content; |
||||
md.dispatchEvent(new Event('input', { bubbles: true })); |
||||
} |
||||
// Title (example: input[name="editor[title]"])
|
||||
const titleInput = document.querySelector('input[name="editor[title]"]'); |
||||
if (titleInput && json.title !== undefined) { |
||||
titleInput.value = json.title; |
||||
titleInput.dispatchEvent(new Event('input', { bubbles: true })); |
||||
} |
||||
// Tags (example: input[name="editor[tags]"] or similar)
|
||||
const tagsInput = document.querySelector('input[name="editor[tags]"]'); |
||||
if (tagsInput && json.tags !== undefined && Array.isArray(json.tags)) { |
||||
tagsInput.value = json.tags.join(', '); |
||||
tagsInput.dispatchEvent(new Event('input', { bubbles: true })); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue