feat: replace custom rich text editor with WordPress TinyMCE and add markdown conversion

- Replace custom contenteditable rich text editor with WordPress native TinyMCE editor
- Implement comprehensive markdown to HTML conversion for AI responses
- Support headers (H1-H3), bold/italic text, bullet lists, and paragraphs
- Integrate markdown conversion into AI Assistant response handling
- Maintain backward compatibility with existing textarea fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ben 2025-09-26 16:18:02 -03:00
parent 00f88070b8
commit b7e5514e8e
2 changed files with 133 additions and 39 deletions

View file

@ -568,17 +568,20 @@ jQuery(document).ready(function($) {
// Apply description (handle TinyMCE, regular textarea, and rich text editor) // Apply description (handle TinyMCE, regular textarea, and rich text editor)
if (data.description) { if (data.description) {
// Convert markdown to HTML for proper rich text editor formatting
const htmlContent = this.markdownToHtml(data.description);
// Try TinyMCE first if available // Try TinyMCE first if available
if (typeof tinyMCE !== 'undefined' && tinyMCE.get('event_description')) { if (typeof tinyMCE !== 'undefined' && tinyMCE.get('event_description')) {
tinyMCE.get('event_description').setContent(data.description); tinyMCE.get('event_description').setContent(htmlContent);
} else { } else {
// Update the hidden textarea // Update the hidden textarea with HTML content
$('#event_description, [name="event_description"]').val(data.description); $('#event_description, [name="event_description"]').val(htmlContent);
// Also update the visible rich text editor div if it exists // Also update the visible rich text editor div if it exists
const $richEditor = $('#event-description-editor'); const $richEditor = $('#event-description-editor');
if ($richEditor.length && $richEditor.is('[contenteditable]')) { if ($richEditor.length && $richEditor.is('[contenteditable]')) {
$richEditor.html(data.description); $richEditor.html(htmlContent);
} }
} }
} }
@ -707,6 +710,85 @@ jQuery(document).ready(function($) {
alert(message); alert(message);
}, },
/**
* Convert markdown to HTML for rich text editor
*/
markdownToHtml: function(markdown) {
let html = markdown;
// Convert headers (## -> h2, ### -> h3, etc.)
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Convert bold text (**text** -> <strong>text</strong>)
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Convert italic text (*text* -> <em>text</em>)
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Convert bullet lists (* item -> <ul><li>item</li></ul>)
html = html.replace(/^\* (.+)$/gm, '<li>$1</li>');
// Wrap consecutive <li> items in <ul> tags
html = html.replace(/(<li>.*<\/li>)/gs, function(match) {
if (!match.includes('<ul>')) {
return '<ul>' + match + '</ul>';
}
return match;
});
// Convert line breaks to <p> tags for paragraphs
const lines = html.split('\n');
const paragraphs = [];
let currentParagraph = '';
for (let line of lines) {
line = line.trim();
// Skip empty lines
if (line === '') {
if (currentParagraph) {
paragraphs.push(currentParagraph);
currentParagraph = '';
}
continue;
}
// If line is already wrapped in HTML tags, add it as is
if (line.match(/^<(h[1-6]|ul|li|strong|em)/)) {
if (currentParagraph) {
paragraphs.push(currentParagraph);
currentParagraph = '';
}
paragraphs.push(line);
} else {
// Regular text line
if (currentParagraph) {
currentParagraph += ' ' + line;
} else {
currentParagraph = line;
}
}
}
// Add final paragraph if exists
if (currentParagraph) {
paragraphs.push(currentParagraph);
}
// Wrap non-HTML paragraphs in <p> tags
const formattedParagraphs = paragraphs.map(p => {
if (p.match(/^<(h[1-6]|ul|li)/)) {
return p;
} else {
return '<p>' + p + '</p>';
}
});
return formattedParagraphs.join('\n');
},
/** /**
* Escape HTML for safe display * Escape HTML for safe display
*/ */

View file

@ -307,44 +307,11 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
'required' => true, 'required' => true,
]); ]);
// Event description with rich text editor // Event description with WordPress rich text editor
$description_field = [ $description_field = [
'type' => 'custom', 'type' => 'custom',
'name' => 'event_description', 'name' => 'event_description',
'custom_html' => '<div class="form-row event-description-wrapper"> 'custom_html' => $this->render_wp_editor_field(),
<label for="event_description"><strong>Event Description</strong></label>
<div id="event-description-editor-wrapper" class="rich-text-editor-wrapper">
<div id="event-description-toolbar" class="rich-text-toolbar">
<div class="toolbar-group">
<button type="button" data-command="bold" title="Bold"><strong>B</strong></button>
<button type="button" data-command="italic" title="Italic"><em>I</em></button>
<button type="button" data-command="underline" title="Underline"><u>U</u></button>
</div>
<div class="toolbar-group">
<button type="button" data-command="insertUnorderedList" title="Bullet List"> List</button>
<button type="button" data-command="insertOrderedList" title="Numbered List">1. List</button>
</div>
<div class="toolbar-group">
<button type="button" data-command="createLink" title="Insert Link">🔗 Link</button>
<button type="button" data-command="unlink" title="Remove Link">🔗✗</button>
</div>
</div>
<div
id="event-description-editor"
class="rich-text-editor"
contenteditable="true"
data-placeholder="Describe your event... Include key details like what attendees will learn, what to bring, prerequisites, and any special requirements."
style="min-height: 200px; border: 1px solid #ddd; padding: 15px; border-radius: 4px;"
></div>
<textarea
name="event_description"
id="event_description"
style="display: none;"
maxlength="5000"
></textarea>
</div>
<small class="description">Use the toolbar above to format your event description. Character limit: 5000</small>
</div>',
'wrapper_class' => 'form-row event-description-field' 'wrapper_class' => 'form-row event-description-field'
]; ];
@ -1665,4 +1632,49 @@ HTML;
</div> </div>
HTML; HTML;
} }
/**
* Render WordPress rich text editor field
*
* @return string HTML for WordPress editor
*/
private function render_wp_editor_field(): string {
ob_start();
?>
<div class="form-row event-description-wrapper">
<label for="event_description"><strong>Event Description</strong></label>
<?php
$editor_settings = [
'textarea_name' => 'event_description',
'textarea_rows' => 10,
'media_buttons' => false,
'teeny' => false,
'tinymce' => [
'toolbar1' => 'formatselect,bold,italic,underline,strikethrough,|,bullist,numlist,|,link,unlink,|,blockquote,hr,|,alignleft,aligncenter,alignright,|,undo,redo',
'toolbar2' => '',
'block_formats' => 'Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Preformatted=pre',
'forced_root_block' => 'p',
'force_p_newlines' => true,
'remove_redundant_brs' => true,
'convert_urls' => false,
'paste_as_text' => false,
'paste_auto_cleanup_on_paste' => true,
'paste_remove_spans' => true,
'paste_remove_styles' => true,
'paste_strip_class_attributes' => 'all',
'valid_elements' => 'p,br,strong,em,ul,ol,li,h2,h3,h4,h5,h6,blockquote,a[href|title],hr',
'valid_children' => '+p[strong|em|a|br],+ul[li],+ol[li],+li[strong|em|a|br|p]'
],
'quicktags' => [
'buttons' => 'strong,em,ul,ol,li,link,close'
]
];
wp_editor('', 'event_description', $editor_settings);
?>
<small class="description">Use the editor above to format your event description with headings, lists, and formatting.</small>
</div>
<?php
return ob_get_clean();
}
} }