$(document).ready(function() { // Prevents resubmit on forms if (window.history.replaceState) { window.history.replaceState(null, null, window.location.href); } // Slide alert up after 4 secs $("#alert").fadeTo(5000, 500).slideUp(500, function() { $("#alert").slideUp(500); }); // Initialize Select2 Elements $('.select2').select2({ theme: 'bootstrap4', }); // Initialize TinyMCE tinymce.init({ selector: '.tinymce-simple', browser_spellcheck: true, contextmenu: false, resize: true, min_height: 300, max_height: 600, promotion: false, branding: false, menubar: false, statusbar: false, toolbar: [ { name: 'styles', items: ['styles'] }, { name: 'formatting', items: ['bold', 'italic', 'forecolor'] }, { name: 'link', items: ['link'] }, { name: 'lists', items: ['bullist', 'numlist'] }, { name: 'alignment', items: ['alignleft', 'aligncenter', 'alignright', 'alignjustify'] }, { name: 'indentation', items: ['outdent', 'indent'] }, { name: 'table', items: ['table'] }, { name: 'extra', items: ['code', 'fullscreen'] } ], mobile: { menubar: false, plugins: 'autosave lists autolink', toolbar: 'bold italic styles' }, convert_urls: false, plugins: 'link image lists table code codesample fullscreen autoresize', setup: function (editor) { editor.on('init', function() { window.onbeforeunload = function() { // If editor is dirty AND not inside a visible modal → warn const inVisibleModal = editor.getContainer()?.closest('.modal.show'); if (!inVisibleModal && editor.isDirty()) { return "You have unsaved changes. Are you sure you want to leave?"; } }; // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { modal.addEventListener('hidden.bs.modal', () => { editor.undoManager.clear(); editor.setDirty(false); }); } }); }, license_key: 'gpl' }); // Initialize TinyMCE with AI tinymce.init({ selector: '.tinymce', browser_spellcheck: true, contextmenu: false, resize: true, min_height: 300, max_height: 600, promotion: false, branding: false, menubar: false, statusbar: false, toolbar: [ { name: 'styles', items: ['styles'] }, { name: 'formatting', items: ['bold', 'italic', 'forecolor'] }, { name: 'link', items: ['link'] }, { name: 'lists', items: ['bullist', 'numlist'] }, { name: 'alignment', items: ['alignleft', 'aligncenter', 'alignright', 'alignjustify'] }, { name: 'indentation', items: ['outdent', 'indent'] }, { name: 'table', items: ['table'] }, { name: 'extra', items: ['code', 'fullscreen'] }, { name: 'ai', items: ['reword', 'undo', 'redo'] } ], mobile: { menubar: false, plugins: 'autosave lists autolink', toolbar: 'bold italic styles' }, convert_urls: false, plugins: 'link image lists table code codesample fullscreen autoresize', license_key: 'gpl', setup: function(editor) { editor.on('init', function() { window.onbeforeunload = function() { // If editor is dirty AND not inside a visible modal → warn const inVisibleModal = editor.getContainer()?.closest('.modal.show'); if (!inVisibleModal && editor.isDirty()) { return "You have unsaved changes. Are you sure you want to leave?"; } }; // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { modal.addEventListener('hidden.bs.modal', () => { editor.undoManager.clear(); editor.setDirty(false); }); } }); var rewordButtonApi; editor.ui.registry.addButton('reword', { icon: 'ai', tooltip: 'Reword Text', onAction: function() { var content = editor.getContent(); // Disable the Reword button rewordButtonApi.setEnabled(false); // Show the progress indicator editor.setProgressState(true); fetch('ajax.php?ai_reword', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: content }), }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { editor.undoManager.transact(function() { editor.setContent(data.rewordedText || 'Error: Could not reword the text.'); }); editor.setProgressState(false); rewordButtonApi.setEnabled(true); editor.notificationManager.open({ text: 'Text reworded successfully!', type: 'success', timeout: 3000 }); }) .catch(error => { console.error('Error:', error); editor.setProgressState(false); rewordButtonApi.setEnabled(true); editor.notificationManager.open({ text: 'An error occurred while rewording the text.', type: 'error', timeout: 5000 }); }); }, onSetup: function(buttonApi) { rewordButtonApi = buttonApi; return function() {}; } }); } }); // Initialize TinyMCE AI for Tickets tinymce.init({ selector: '.tinymceTicket', browser_spellcheck: true, contextmenu: false, resize: true, min_height: 200, max_height: 600, promotion: false, branding: false, menubar: false, statusbar: false, toolbar: [ { name: 'styles', items: ['styles'] }, { name: 'formatting', items: ['bold', 'italic', 'forecolor'] }, { name: 'link', items: ['link'] }, { name: 'lists', items: ['bullist', 'numlist'] }, { name: 'indentation', items: ['outdent', 'indent'] }, { name: 'ai', items: ['reword', 'undo', 'redo'] }, { name: 'custom', items: ['redactButton'] }, { name: 'code', items: ['code'] }, ], mobile: { menubar: false, toolbar: [ { name: 'styles', items: ['styles'] }, { name: 'formatting', items: ['bold', 'italic', 'forecolor'] }, { name: 'link', items: ['link'] }, { name: 'lists', items: ['bullist', 'numlist'] }, { name: 'indentation', items: ['outdent', 'indent'] }, { name: 'ai', items: ['reword', 'undo', 'redo'] }, { name: 'custom', items: ['redactButton'] }, { name: 'code', items: ['code'] }, ], }, convert_urls: false, plugins: 'link image lists table code codesample fullscreen autoresize code', license_key: 'gpl', setup: function(editor) { editor.on('init', function() { window.onbeforeunload = function() { // If editor is dirty AND not inside a visible modal → warn const inVisibleModal = editor.getContainer()?.closest('.modal.show'); if (!inVisibleModal && editor.isDirty()) { return "You have unsaved changes. Are you sure you want to leave?"; } }; // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { modal.addEventListener('hidden.bs.modal', () => { editor.undoManager.clear(); editor.setDirty(false); }); } }); var rewordButtonApi; editor.ui.registry.addButton('reword', { icon: 'ai', tooltip: 'Reword Text', onAction: function() { var content = editor.getContent(); rewordButtonApi.setEnabled(false); editor.setProgressState(true); fetch('ajax.php?ai_reword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: content }), }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .then(data => { editor.undoManager.transact(function() { editor.setContent(data.rewordedText || 'Error: Could not reword the text.'); }); editor.setProgressState(false); rewordButtonApi.setEnabled(true); editor.notificationManager.open({ text: 'Text reworded successfully!', type: 'success', timeout: 3000 }); }) .catch(error => { console.error('Error:', error); editor.setProgressState(false); rewordButtonApi.setEnabled(true); editor.notificationManager.open({ text: 'An error occurred while rewording the text.', type: 'error', timeout: 5000 }); }); }, onSetup: function(buttonApi) { rewordButtonApi = buttonApi; return function() {}; } }); editor.ui.registry.addButton('redactButton', { icon: 'permanent-pen', tooltip: 'Redact Text', onAction: function() { var selectedText = editor.selection.getContent({ format: 'text' }); if (selectedText) { var newContent = '[REDACTED]'; editor.selection.setContent(newContent); } else { alert('Please select a word to redact'); } } }); } }); // Initialize TinyMCE Redact-only tinymce.init({ selector: '.tinymceRedact', browser_spellcheck: true, contextmenu: false, resize: true, min_height: 300, max_height: 600, promotion: false, branding: false, menubar: false, statusbar: false, toolbar: 'redactButton', mobile: { menubar: false, plugins: 'autosave lists autolink', toolbar: 'redactButton' }, convert_urls: false, plugins: 'link image lists table code fullscreen autoresize', license_key: 'gpl', setup: function(editor) { editor.on('init', function() { window.onbeforeunload = function() { // If editor is dirty AND not inside a visible modal → warn const inVisibleModal = editor.getContainer()?.closest('.modal.show'); if (!inVisibleModal && editor.isDirty()) { return "You have unsaved changes. Are you sure you want to leave?"; } }; // When the modal closes, mark editor clean const modal = editor.getContainer()?.closest('.modal'); if (modal) { modal.addEventListener('hidden.bs.modal', () => { editor.undoManager.clear(); editor.setDirty(false); }); } }); editor.on('keydown', function(e) { e.preventDefault(); }); editor.ui.registry.addButton('redactButton', { icon: 'permanent-pen', tooltip: 'Redact', text: 'REDACT', onAction: function() { var selectedText = editor.selection.getContent({ format: 'text' }); if (selectedText) { var newContent = '[REDACTED]'; editor.selection.setContent(newContent); } else { alert('Please select a word to redact'); } } }); } }); // DateTime $('.datetimepicker').datetimepicker(); // Data Input Mask $('[data-mask]').inputmask(); // ClipboardJS fix for Bootstrap modals $.fn.modal.Constructor.prototype._enforceFocus = function() {}; // Clipboard var clipboard = new ClipboardJS('.clipboardjs'); clipboard.on('success', function(e) { flashTooltip(e.trigger, 'Copied!'); }); clipboard.on('error', function(e) { flashTooltip(e.trigger, 'Failed!'); }); // Enable Popovers $(function() { $('[data-toggle="popover"]').popover(); }); // Data Tables new DataTable('.dataTables'); }); /* * Calendar event modals - the All day switch shows or hides the time row. * * The form uses four separate fields (date from / date to / time from / time to), * so nothing here rewrites a value or changes an input type. An earlier version * flipped a single datetime-local input to type="date", which a browser answers by * silently discarding a value it now considers invalid - a fragile arrangement that * left the fields empty whenever this handler had not run. * * required tracks visibility: a hidden required field blocks form submission with an * unfocusable-element error that the user cannot act on. * * Delegated on document because the edit event modal is injected by ajax. The * namespaced .off() keeps this to a single handler - modal_footer.php re-loads this * file on every ajax modal open. */ $(document).off('change.itflowAllDay').on('change.itflowAllDay', '.event-all-day-toggle', function () { const allDay = this.checked; const timeFields = document.getElementById(this.id.replace(/_all_day$/, '_time_fields')); if (!timeFields) { return; } timeFields.classList.toggle('d-none', allDay); timeFields.querySelectorAll('input').forEach(function (field) { if (allDay) { field.removeAttribute('required'); } else { field.setAttribute('required', 'required'); } }); }); /* * Keep the end date at or after the start date, without shortening a longer span * the user has already chosen. */ $(document).off('change.itflowEventDate').on('change.itflowEventDate', '.event-start-date', function () { const endField = document.getElementById(this.id.replace(/_start_date$/, '_end_date')); if (!endField || !this.value) { return; } if (!endField.value || endField.value < this.value) { endField.value = this.value; } }); /* * Default the end time to an hour after the start, leaving a longer span alone. * A start late in the evening rolls the end onto the following day rather than * wrapping round to an end that precedes the start. */ $(document).off('change.itflowEventTime').on('change.itflowEventTime', '.event-start-time', function () { const prefix = this.id.replace(/_start_time$/, ''); const endField = document.getElementById(prefix + '_end_time'); if (!endField || !this.value) { return; } if (endField.value && endField.value > this.value) { return; } const parts = this.value.split(':'); const end = new Date(2000, 0, 1, Number(parts[0]), Number(parts[1])); end.setHours(end.getHours() + 1); const pad = (n) => String(n).padStart(2, '0'); endField.value = pad(end.getHours()) + ':' + pad(end.getMinutes()); // Crossed midnight - carry the end date forward so the event still ends after // it starts if (end.getDate() !== 1) { const startDate = document.getElementById(prefix + '_start_date'); const endDate = document.getElementById(prefix + '_end_date'); if (startDate && endDate && startDate.value && endDate.value <= startDate.value) { const next = new Date(startDate.value + 'T00:00:00'); next.setDate(next.getDate() + 1); endDate.value = next.getFullYear() + '-' + pad(next.getMonth() + 1) + '-' + pad(next.getDate()); } } }); /* * Bootstrap tooltips are only used for the brief "Copied!" flash on a copy * button. Everything else relies on the browser's own title attribute. * * This used to be `$('button').tooltip({trigger: 'click'})`, which attached a * click-toggled tooltip to EVERY button in the app. A click-triggered tooltip * is only dismissed by clicking the same button again - clicking anywhere else * leaves it on screen - and if the button was then removed from the DOM by a * modal closing or an ajax refresh, its tooltip was orphaned and stayed up * until the next page load. */ function flashTooltip(button, message) { $(button) .tooltip('dispose') .tooltip({ trigger: 'manual', placement: 'bottom', title: message }) .tooltip('show'); setTimeout(function() { $(button).tooltip('dispose'); }, 1000); }