// Initialize EasyMDE for the notes textarea if on add/edit page if (typeof EasyMDE !== 'undefined') { const notes = new EasyMDE({ element: document.getElementById('notes'), forceSync: true, spellChecker: false, placeholder: 'Create note...', maxHeight: '350px', toolbar: [ { name: "bold", action: EasyMDE.toggleBold, className: "fa fa-bold", title: "bold", }, { name: "italic", action: EasyMDE.toggleItalic, className: "fa fa-italic", title: "italic", }, { name: "heading", action: EasyMDE.toggleHeadingSmaller, className: "fa fa-header", title: "heading", }, "|", { name: "quote", action: EasyMDE.toggleBlockquote, className: "fa fa-quote-left", title: "quote", }, { name: "unordered-list", action: EasyMDE.toggleUnorderedList, className: "fa fa-list-ul", title: "unordered list", }, { name: "ordered-list", action: EasyMDE.toggleOrderedList, className: "fa fa-list-ol", title: "ordered list", }, "|", { name: "link", action: EasyMDE.drawLink, className: "fa fa-link", title: "link" }, { name: "image", action: EasyMDE.drawImage, className: "fa fa-image", title: "image" }, "|", { name: "preview", action: EasyMDE.togglePreview, className: "fa fa-eye no-disable", title: "preview" }, "|", { name: "guide", action: "https://www.markdownguide.org/basic-syntax/", // link className: "fa fa-question-circle", title: "Markdown Guide" } ] }); // Always sync EasyMDE content to textarea before form submit var notesForm = document.getElementById('notes_add'); if (notesForm) { notesForm.addEventListener('submit', function(e) { if (notes && notes.codemirror) { notes.codemirror.save(); // forceSync } }); } // If validation fails and textarea has value, restore it to EasyMDE var notesTextarea = document.getElementById('notes'); if (notesTextarea && notesTextarea.value && notes.value() !== notesTextarea.value) { notes.value(notesTextarea.value); } } // Main notes page functionality document.addEventListener('DOMContentLoaded', function() { // Replace 0 with Ø in inputTitle as user types var inputTitle = document.getElementById('inputTitle'); if (inputTitle) { inputTitle.addEventListener('input', function() { var caret = inputTitle.selectionStart; var newValue = inputTitle.value.replace(/0/g, 'Ø'); if (inputTitle.value !== newValue) { inputTitle.value = newValue; inputTitle.setSelectionRange(caret, caret); } }); } // Early exit if we're not on a notes page var notesTableBody = document.querySelector('#notesTable tbody'); var isNotesMainPage = notesTableBody !== null; var base_url = window.base_url || document.body.getAttribute('data-baseurl') || '/'; // Constants const NOTES_PER_PAGE = 15; const SEARCH_MIN_LENGTH = 3; const SORT_COLUMN_MAP = ['cat', 'title', 'creation_date', 'last_modified', null]; // Cache frequently used DOM elements to avoid repeated queries var domCache = { notesTableBody: notesTableBody, notesTable: document.getElementById('notesTable'), categoryButtons: document.querySelectorAll('.category-btn'), searchBox: document.getElementById('notesSearchBox'), resetBtn: document.getElementById('notesSearchReset'), titleInput: document.getElementById('inputTitle'), catSelect: document.getElementById('catSelect'), saveBtn: document.querySelector('button[type="submit"]'), form: document.getElementById('notes_add'), paginationContainer: document.getElementById('notesPagination') }; // Create pagination container if it doesn't exist if (!domCache.paginationContainer) { domCache.paginationContainer = document.createElement('div'); domCache.paginationContainer.id = 'notesPagination'; domCache.paginationContainer.className = 'd-flex justify-content-center my-3'; var notesTableContainer = document.getElementById('notesTableContainer'); if (notesTableContainer) { notesTableContainer.appendChild(domCache.paginationContainer); } } // Initialize existing UTC time cells and tooltips on page load // Helper function to initialize table elements after rendering function initializeTableElements() { // Convert UTC times to local time document.querySelectorAll('#notesTable td[data-utc]').forEach(function(td) { var utc = td.getAttribute('data-utc'); td.textContent = utcToLocal(utc); }); // Initialize Bootstrap tooltips for note titles var tooltipTriggerList = [].slice.call(document.querySelectorAll('#notesTable a[data-bs-toggle="tooltip"]')); tooltipTriggerList.forEach(function(el) { new bootstrap.Tooltip(el); }); } // Run initialization for existing table content initializeTableElements(); // Duplicate Contacts note check for add/edit pages var modal; function showModal(msg) { if (!modal) { modal = document.createElement('div'); modal.className = 'modal fade'; modal.innerHTML = ''; document.body.appendChild(modal); } modal.querySelector('.modal-body p').textContent = msg; $(modal).modal('show'); } // Reload category counters via AJAX function reloadCategoryCounters() { fetch(base_url + 'index.php/notes/get_category_counts', { method: 'POST' }) .then(response => response.json()) .then(data => { domCache.categoryButtons.forEach(function(btn) { var cat = btn.getAttribute('data-category'); var countSpan = btn.querySelector('.badge'); if (countSpan) { if (cat === '__all__') { // Handle "All Categories" button countSpan.textContent = data.all_notes_count; } else if (data.category_counts && data.category_counts[cat] !== undefined) { // Handle specific category buttons countSpan.textContent = data.category_counts[cat]; } } }); }); } // Helper: Convert UTC string to browser local time function utcToLocal(utcString) { if (!utcString) return ''; // Parse as UTC var utcDate = new Date(utcString + ' UTC'); if (isNaN(utcDate.getTime())) return utcString; return utcDate.toLocaleString(); } // Get currently active category function getActiveCategory() { var activeBtn = document.querySelector('.category-btn.active'); if (activeBtn) { var cat = activeBtn.getAttribute('data-category'); return cat === '__all__' ? '' : cat; } return ''; } // Perform search and update table function performNotesSearch() { var searchTerm = searchBox ? searchBox.value.trim() : ''; var selectedCat = getActiveCategory(); var formData = new FormData(); formData.append('cat', selectedCat); // Only send search if 3+ chars, else send empty search if (searchTerm.length >= 3) { formData.append('search', searchTerm); } else { formData.append('search', ''); } fetch(base_url + 'index.php/notes/search', { method: 'POST', body: formData }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .then(data => { var tbody = ''; if (data.length === 0) { tbody = '' + lang_notes_not_found + ''; } else { data.forEach(function(note) { tbody += '' + '' + (note.cat ? note.cat : '') + '' + '' + (note.title ? note.title : '') + '' + '' + (note.last_modified ? note.last_modified : '') + '' + ''; }); } if (notesTableBody) { notesTableBody.innerHTML = tbody; } }) .catch(error => { if (notesTableBody) { notesTableBody.innerHTML = '' + lang_notes_error_loading + ': ' + error.message + ''; } }); } // Sorting logic for notes table var sortState = { column: 3, // Default to 'Last Modification' column direction: 'desc' // Show latest modified at top }; var columnHeaders = domCache.notesTable ? domCache.notesTable.querySelectorAll('thead th') : []; // Add sorting indicators and click handlers (only for supported columns) columnHeaders.forEach(function(th, idx) { var span = document.createElement('span'); span.className = 'dt-column-order'; th.appendChild(span); if (SORT_COLUMN_MAP[idx]) { span.setAttribute('role', 'button'); span.setAttribute('aria-label', th.textContent + ': ' + lang_notes_sort); span.setAttribute('tabindex', '0'); th.style.cursor = 'pointer'; th.addEventListener('click', function() { if (sortState.column !== idx) { sortState.column = idx; sortState.direction = 'asc'; } else if (sortState.direction === 'asc') { sortState.direction = 'desc'; } else if (sortState.direction === 'desc') { sortState.direction = null; sortState.column = null; } else { sortState.direction = 'asc'; } updateSortIndicators(); performNotesSearch(); }); } else { th.style.cursor = 'default'; } }); // Update sort indicators in the header function updateSortIndicators() { columnHeaders.forEach(function(th, idx) { var span = th.querySelector('.dt-column-order'); if (!span) return; span.textContent = ''; if (sortState.column === idx) { if (sortState.direction === 'asc') { span.textContent = '▲'; } else if (sortState.direction === 'desc') { span.textContent = '▼'; } } }); } // Server-side pagination, sorting, and search integration var currentPage = 1; var totalPages = 1; var lastResponseTotal = 0; window.lastNotesData = []; // Render pagination controls function renderPagination() { if (!domCache.paginationContainer) return; domCache.paginationContainer.innerHTML = ''; if (totalPages <= 1) return; var ul = document.createElement('ul'); ul.className = 'pagination pagination-sm'; for (var i = 1; i <= totalPages; i++) { var li = document.createElement('li'); li.className = 'page-item' + (i === currentPage ? ' active' : ''); var a = document.createElement('a'); a.className = 'page-link'; a.href = '#'; a.textContent = i; a.addEventListener('click', function(e) { e.preventDefault(); var page = parseInt(this.textContent); if (page !== currentPage) { currentPage = page; performNotesSearch(); } }); li.appendChild(a); ul.appendChild(li); } domCache.paginationContainer.appendChild(ul); } // Simple Markdown to plain text conversion for tooltip preview function markdownToText(md) { // Remove code blocks md = md.replace(/```[\s\S]*?```/g, ''); // Remove inline code md = md.replace(/`[^`]*`/g, ''); // Remove images md = md.replace(/!\[.*?\]\(.*?\)/g, ''); // Remove links but keep text md = md.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); // Remove headings md = md.replace(/^#{1,6}\s*/gm, ''); // Remove blockquotes md = md.replace(/^>\s?/gm, ''); // Remove emphasis md = md.replace(/(\*\*|__)(.*?)\1/g, '$2'); md = md.replace(/(\*|_)(.*?)\1/g, '$2'); // Remove lists md = md.replace(/^\s*([-*+]|\d+\.)\s+/gm, ''); // Remove horizontal rules md = md.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, ''); // Remove HTML tags md = md.replace(/<[^>]+>/g, ''); // Collapse whitespace md = md.replace(/\s+/g, ' ').trim(); return md; } // Render notes table with data function renderNotesTable(data) { // Helper function to get translated category name function getTranslatedCategory(categoryKey) { if (window.categoryTranslations && window.categoryTranslations[categoryKey]) { return window.categoryTranslations[categoryKey]; } return categoryKey || ''; } var tbody = ''; if (data.length === 0) { tbody = '' + lang_notes_not_found + ''; } else { data.forEach(function(note) { // Strip HTML/Markdown and truncate to 100 chars for tooltip var rawContent = note.note ? note.note : ''; // Use a more robust Markdown-to-text conversion var plainContent = markdownToText(rawContent); // Truncate to 100 chars var preview = plainContent.length > 100 ? plainContent.substring(0, 100) + '…' : plainContent; tbody += '' + '' + getTranslatedCategory(note.cat) + '' + '' + (note.title ? note.title : '') + '' + '' + '' + '' + '
' + '' + '' + (note.cat === 'Contacts' ? '' : '' ) + '' + '
' + '' + ''; }); } if (domCache.notesTableBody) { domCache.notesTableBody.innerHTML = tbody; // After rendering, initialize table elements initializeTableElements(); } updateSortIndicators(); } // Modal confirmation for delete and duplicate window.confirmDeleteNote = function(noteId) { showBootstrapModal(lang_notes_delete, lang_notes_delete_confirmation, function() { deleteNote(noteId); }); }; window.confirmDuplicateNote = function(noteId) { var note = (window.lastNotesData || []).find(function(n) { return n.id == noteId; }); if (note && note.cat === 'Contacts') { showBootstrapModal(lang_notes_duplication_disabled_short, lang_notes_duplication_disabled, function(){}); return; } showBootstrapModal(lang_notes_duplicate, lang_notes_duplicate_confirmation, function() { duplicateNote(noteId); }); }; // Actions for delete and duplicate function deleteNote(noteId) { fetch(base_url + 'index.php/notes/delete/' + noteId, { method: 'POST' }) .then(() => { // Check if we need to go to previous page after deletion // If we're on the last page and it only has 1 item, go back one page var currentPageItemCount = window.lastNotesData ? window.lastNotesData.length : 0; if (currentPage > 1 && currentPageItemCount === 1) { currentPage = currentPage - 1; } performNotesSearch(); reloadCategoryCounters(); }); } // Duplicate note via POST with timestamp function duplicateNote(noteId) { // Get local timestamp var now = new Date(); var timestamp = now.toLocaleString(); var formData = new FormData(); formData.append('timestamp', timestamp); fetch(base_url + 'index.php/notes/duplicate/' + noteId, { method: 'POST', body: formData }) .then(() => { performNotesSearch(); reloadCategoryCounters(); }); } // Bootstrap modal helper function showBootstrapModal(title, message, onConfirm) { var modalId = 'confirmModal_' + Math.random().toString(36).substr(2, 9); var modalHtml = ''; var modalDiv = document.createElement('div'); modalDiv.innerHTML = modalHtml; document.body.appendChild(modalDiv); var modalEl = modalDiv.querySelector('.modal'); var modal; try { modal = new bootstrap.Modal(modalEl, { backdrop: 'static' }); modal.show(); } catch (e) { document.body.removeChild(modalDiv); if (confirm(message)) { onConfirm(); } return; } modalDiv.querySelector('#confirmModalBtn_' + modalId).onclick = function() { modal.hide(); setTimeout(function() { document.body.removeChild(modalDiv); }, 300); onConfirm(); }; modalDiv.querySelector('[data-bs-dismiss="modal"]').onclick = function() { modal.hide(); setTimeout(function() { document.body.removeChild(modalDiv); }, 300); }; } // Patch performNotesSearch to use server-side pagination and sorting performNotesSearch = function() { var searchTerm = domCache.searchBox ? domCache.searchBox.value.trim() : ''; var selectedCat = getActiveCategory(); var sortColIdx = sortState.column; var sortDir = sortState.direction; var sortCol = (sortColIdx !== null && SORT_COLUMN_MAP[sortColIdx]) ? SORT_COLUMN_MAP[sortColIdx] : null; var formData = new FormData(); formData.append('cat', selectedCat); formData.append('search', searchTerm.length >= SEARCH_MIN_LENGTH ? searchTerm : ''); formData.append('page', currentPage); formData.append('per_page', NOTES_PER_PAGE); formData.append('sort_col', sortCol || ''); formData.append('sort_dir', sortDir || ''); fetch(base_url + 'index.php/notes/search', { method: 'POST', body: formData }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .then(resp => { var data = (resp && Array.isArray(resp.notes)) ? resp.notes : []; window.lastNotesData = data; lastResponseTotal = resp.total || 0; totalPages = Math.max(1, Math.ceil(lastResponseTotal / NOTES_PER_PAGE)); if (currentPage > totalPages) currentPage = totalPages; renderNotesTable(data); renderPagination(); reloadCategoryCounters(); }) .catch(error => { if (domCache.notesTableBody) { domCache.notesTableBody.innerHTML = '' + lang_notes_error_loading + ':' + error.message + ''; } if (domCache.paginationContainer) { domCache.paginationContainer.innerHTML = ''; } }); }; // Reset to first page on search, sort, or category change if (domCache.categoryButtons && domCache.notesTableBody) { domCache.categoryButtons.forEach(function(btn) { btn.addEventListener('click', function() { domCache.categoryButtons.forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); currentPage = 1; performNotesSearch(); }); }); } if (domCache.searchBox) { domCache.searchBox.addEventListener('input', function() { currentPage = 1; performNotesSearch(); }); } if (domCache.resetBtn) { domCache.resetBtn.addEventListener('click', function() { if (domCache.searchBox) domCache.searchBox.value = ''; currentPage = 1; performNotesSearch(); }); } // Initial render - only if we have the necessary elements if (domCache.notesTableBody) { performNotesSearch(); } // Add stroked zero (Ø) to search box var addStrokedZeroBtn = document.getElementById('notesAddStrokedZero'); if (addStrokedZeroBtn) { addStrokedZeroBtn.addEventListener('click', function() { var searchBox = domCache.searchBox; if (searchBox) { var currentValue = searchBox.value; var cursorPos = searchBox.selectionStart; // Insert Ø at cursor position var newValue = currentValue.slice(0, cursorPos) + 'Ø' + currentValue.slice(cursorPos); searchBox.value = newValue; // Set cursor position after the inserted character searchBox.focus(); searchBox.setSelectionRange(cursorPos + 1, cursorPos + 1); // Trigger search if minimum length is met if (newValue.length >= SEARCH_MIN_LENGTH) { performNotesSearch(); } } }); } });