2026-01-08 23:35:49 -05:00
|
|
|
/**
|
|
|
|
|
* Simple Markdown Parser for Tinker Tickets
|
|
|
|
|
* Supports basic markdown formatting without external dependencies
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
function parseMarkdown(markdown) {
|
|
|
|
|
if (!markdown) return '';
|
|
|
|
|
|
|
|
|
|
let html = markdown;
|
|
|
|
|
|
|
|
|
|
// Escape HTML first to prevent XSS
|
|
|
|
|
html = html.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>');
|
|
|
|
|
|
2026-01-23 10:01:50 -05:00
|
|
|
// Ticket references (#123456789) - convert to clickable links
|
|
|
|
|
html = html.replace(/#(\d{9})\b/g, '<a href="/ticket/$1" class="ticket-link-ref">#$1</a>');
|
|
|
|
|
|
2026-01-08 23:35:49 -05:00
|
|
|
// Code blocks (```code```)
|
|
|
|
|
html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block"><code>$1</code></pre>');
|
|
|
|
|
|
|
|
|
|
// Inline code (`code`)
|
|
|
|
|
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
|
|
|
|
|
|
|
|
|
// Bold (**text** or __text__)
|
|
|
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
|
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
|
|
|
|
|
|
|
|
// Italic (*text* or _text_)
|
|
|
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
|
|
|
html = html.replace(/_(.+?)_/g, '<em>$1</em>');
|
|
|
|
|
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
// Links [text](url) - only allow safe protocols
|
|
|
|
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
|
|
|
|
|
// Only allow http, https, mailto protocols
|
|
|
|
|
if (/^(https?:|mailto:|\/)/i.test(url)) {
|
|
|
|
|
return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
|
|
|
|
|
}
|
|
|
|
|
// Block potentially dangerous protocols (javascript:, data:, etc.)
|
|
|
|
|
return text;
|
|
|
|
|
});
|
2026-01-08 23:35:49 -05:00
|
|
|
|
|
|
|
|
// Headers (# H1, ## H2, etc.)
|
|
|
|
|
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
|
|
|
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
|
|
|
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
|
|
|
|
|
|
|
|
// Lists
|
|
|
|
|
// Unordered lists (- item or * item)
|
|
|
|
|
html = html.replace(/^\s*[-*]\s+(.+)$/gm, '<li>$1</li>');
|
|
|
|
|
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
|
|
|
|
|
|
|
|
|
// Ordered lists (1. item)
|
|
|
|
|
html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
|
|
|
|
|
|
|
|
|
// Blockquotes (> text)
|
|
|
|
|
html = html.replace(/^>\s+(.+)$/gm, '<blockquote>$1</blockquote>');
|
|
|
|
|
|
|
|
|
|
// Horizontal rules (--- or ***)
|
|
|
|
|
html = html.replace(/^(?:---|___|\*\*\*)$/gm, '<hr>');
|
|
|
|
|
|
|
|
|
|
// Line breaks (two spaces at end of line or double newline)
|
|
|
|
|
html = html.replace(/ \n/g, '<br>');
|
|
|
|
|
html = html.replace(/\n\n/g, '</p><p>');
|
|
|
|
|
|
|
|
|
|
// Wrap in paragraph if not already wrapped
|
|
|
|
|
if (!html.startsWith('<')) {
|
|
|
|
|
html = '<p>' + html + '</p>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply markdown rendering to all elements with data-markdown attribute
|
|
|
|
|
function renderMarkdownElements() {
|
|
|
|
|
document.querySelectorAll('[data-markdown]').forEach(element => {
|
|
|
|
|
const markdownText = element.getAttribute('data-markdown') || element.textContent;
|
|
|
|
|
element.innerHTML = parseMarkdown(markdownText);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply markdown to description and comments on page load
|
|
|
|
|
document.addEventListener('DOMContentLoaded', renderMarkdownElements);
|
|
|
|
|
|
|
|
|
|
// Expose for manual use
|
|
|
|
|
window.parseMarkdown = parseMarkdown;
|
|
|
|
|
window.renderMarkdownElements = renderMarkdownElements;
|
Implement comprehensive improvement plan (Phases 1-6)
Security (Phase 1-2):
- Add SecurityHeadersMiddleware with CSP, X-Frame-Options, etc.
- Add RateLimitMiddleware for API rate limiting
- Add security event logging to AuditLogModel
- Add ResponseHelper for standardized API responses
- Update config.php with security constants
Database (Phase 3):
- Add migration 014 for additional indexes
- Add migration 015 for ticket dependencies
- Add migration 016 for ticket attachments
- Add migration 017 for recurring tickets
- Add migration 018 for custom fields
Features (Phase 4-5):
- Add ticket dependencies with DependencyModel and API
- Add duplicate detection with check_duplicates API
- Add file attachments with AttachmentModel and upload/download APIs
- Add @mentions with autocomplete and highlighting
- Add quick actions on dashboard rows
Collaboration (Phase 5):
- Add mention extraction in CommentModel
- Add mention autocomplete dropdown in ticket.js
- Add mention highlighting CSS styles
Admin & Export (Phase 6):
- Add StatsModel for dashboard widgets
- Add dashboard stats cards (open, critical, unassigned, etc.)
- Add CSV/JSON export via export_tickets API
- Add rich text editor toolbar in markdown.js
- Add RecurringTicketModel with cron job
- Add CustomFieldModel for per-category fields
- Add admin views: RecurringTickets, CustomFields, Workflow,
Templates, AuditLog, UserActivity
- Add admin APIs: manage_workflows, manage_templates,
manage_recurring, custom_fields, get_users
- Add admin routes in index.php
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 09:55:01 -05:00
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// Rich Text Editor Toolbar Functions
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert markdown formatting around selection
|
|
|
|
|
*/
|
|
|
|
|
function insertMarkdownFormat(textareaId, prefix, suffix) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const end = textarea.selectionEnd;
|
|
|
|
|
const text = textarea.value;
|
|
|
|
|
const selectedText = text.substring(start, end);
|
|
|
|
|
|
|
|
|
|
// Insert formatting
|
|
|
|
|
const newText = text.substring(0, start) + prefix + selectedText + suffix + text.substring(end);
|
|
|
|
|
textarea.value = newText;
|
|
|
|
|
|
|
|
|
|
// Set cursor position
|
|
|
|
|
if (selectedText) {
|
|
|
|
|
textarea.setSelectionRange(start + prefix.length, end + prefix.length);
|
|
|
|
|
} else {
|
|
|
|
|
textarea.setSelectionRange(start + prefix.length, start + prefix.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
|
|
|
|
// Trigger input event to update preview if enabled
|
|
|
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert markdown at cursor position
|
|
|
|
|
*/
|
|
|
|
|
function insertMarkdownText(textareaId, text) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const value = textarea.value;
|
|
|
|
|
|
|
|
|
|
textarea.value = value.substring(0, start) + text + value.substring(start);
|
|
|
|
|
textarea.setSelectionRange(start + text.length, start + text.length);
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
|
|
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Toolbar button handlers
|
|
|
|
|
*/
|
|
|
|
|
function toolbarBold(textareaId) {
|
|
|
|
|
insertMarkdownFormat(textareaId, '**', '**');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarItalic(textareaId) {
|
|
|
|
|
insertMarkdownFormat(textareaId, '_', '_');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarCode(textareaId) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
|
|
|
|
|
|
|
|
|
// Use code block for multi-line, inline code for single line
|
|
|
|
|
if (selectedText.includes('\n')) {
|
|
|
|
|
insertMarkdownFormat(textareaId, '```\n', '\n```');
|
|
|
|
|
} else {
|
|
|
|
|
insertMarkdownFormat(textareaId, '`', '`');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarLink(textareaId) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
|
|
|
|
|
|
|
|
|
if (selectedText) {
|
|
|
|
|
// Wrap selected text as link text
|
|
|
|
|
insertMarkdownFormat(textareaId, '[', '](url)');
|
|
|
|
|
} else {
|
|
|
|
|
insertMarkdownText(textareaId, '[link text](url)');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarList(textareaId) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const text = textarea.value;
|
|
|
|
|
|
|
|
|
|
// Find start of current line
|
|
|
|
|
let lineStart = start;
|
|
|
|
|
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
|
|
|
|
lineStart--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert list marker at beginning of line
|
|
|
|
|
textarea.value = text.substring(0, lineStart) + '- ' + text.substring(lineStart);
|
|
|
|
|
textarea.setSelectionRange(start + 2, start + 2);
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
|
|
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarHeading(textareaId) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const text = textarea.value;
|
|
|
|
|
|
|
|
|
|
// Find start of current line
|
|
|
|
|
let lineStart = start;
|
|
|
|
|
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
|
|
|
|
lineStart--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert heading marker at beginning of line
|
|
|
|
|
textarea.value = text.substring(0, lineStart) + '## ' + text.substring(lineStart);
|
|
|
|
|
textarea.setSelectionRange(start + 3, start + 3);
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
|
|
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toolbarQuote(textareaId) {
|
|
|
|
|
const textarea = document.getElementById(textareaId);
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const start = textarea.selectionStart;
|
|
|
|
|
const text = textarea.value;
|
|
|
|
|
|
|
|
|
|
// Find start of current line
|
|
|
|
|
let lineStart = start;
|
|
|
|
|
while (lineStart > 0 && text[lineStart - 1] !== '\n') {
|
|
|
|
|
lineStart--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert quote marker at beginning of line
|
|
|
|
|
textarea.value = text.substring(0, lineStart) + '> ' + text.substring(lineStart);
|
|
|
|
|
textarea.setSelectionRange(start + 2, start + 2);
|
|
|
|
|
textarea.focus();
|
|
|
|
|
|
|
|
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create and insert toolbar HTML for a textarea
|
|
|
|
|
*/
|
|
|
|
|
function createEditorToolbar(textareaId, containerId) {
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
const toolbar = document.createElement('div');
|
|
|
|
|
toolbar.className = 'editor-toolbar';
|
|
|
|
|
toolbar.innerHTML = `
|
|
|
|
|
<button type="button" onclick="toolbarBold('${textareaId}')" title="Bold (Ctrl+B)"><b>B</b></button>
|
|
|
|
|
<button type="button" onclick="toolbarItalic('${textareaId}')" title="Italic (Ctrl+I)"><i>I</i></button>
|
|
|
|
|
<button type="button" onclick="toolbarCode('${textareaId}')" title="Code"></></button>
|
|
|
|
|
<span class="toolbar-separator"></span>
|
|
|
|
|
<button type="button" onclick="toolbarHeading('${textareaId}')" title="Heading">H</button>
|
|
|
|
|
<button type="button" onclick="toolbarList('${textareaId}')" title="List">≡</button>
|
|
|
|
|
<button type="button" onclick="toolbarQuote('${textareaId}')" title="Quote">"</button>
|
|
|
|
|
<span class="toolbar-separator"></span>
|
|
|
|
|
<button type="button" onclick="toolbarLink('${textareaId}')" title="Link">🔗</button>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
container.insertBefore(toolbar, container.firstChild);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expose toolbar functions globally
|
|
|
|
|
window.toolbarBold = toolbarBold;
|
|
|
|
|
window.toolbarItalic = toolbarItalic;
|
|
|
|
|
window.toolbarCode = toolbarCode;
|
|
|
|
|
window.toolbarLink = toolbarLink;
|
|
|
|
|
window.toolbarList = toolbarList;
|
|
|
|
|
window.toolbarHeading = toolbarHeading;
|
|
|
|
|
window.toolbarQuote = toolbarQuote;
|
|
|
|
|
window.createEditorToolbar = createEditorToolbar;
|
|
|
|
|
window.insertMarkdownFormat = insertMarkdownFormat;
|
|
|
|
|
window.insertMarkdownText = insertMarkdownText;
|