/home/edulekha/crm.edulekha.com/modules/appointly/assets/js/appointly_calendar_tooltips.js
/**
* Appointly Calendar Tooltips - Prevent HTML Code Overlay
*/
(function($) {
'use strict';
$(document).ready(function() {
// Initialize calendar time format cleanup
initializeCalendarCleanup();
// Initialize tooltips
setTimeout(enhanceCalendarEvents, 1000);
// Refresh on calendar navigation and view changes
$(document).on('click', '.fc-button', function() {
setTimeout(function() {
cleanUpCalendarTimes();
enhanceCalendarEvents();
}, 300);
});
// Enhanced navigation detection for FullCalendar
setInterval(function() {
// Check if new events have been loaded that need processing
var unprocessedEvents = $('.fc-event[href*="appointly/appointments/view"]:not(.appointly-event)');
if (unprocessedEvents.length > 0) {
cleanUpCalendarTimes();
enhanceCalendarEvents();
}
}, 500);
});
// Helper function to convert 12-hour time to 24-hour format
function convertTo24Hour(hour, minute, isPM) {
hour = parseInt(hour);
minute = minute || '00';
if (isPM && hour !== 12) {
hour += 12;
} else if (!isPM && hour === 12) {
hour = 0;
}
return (hour < 10 ? '0' : '') + hour + ':' + minute;
}
// Helper function to convert time format based on system setting
function formatTimePrefix(titleText) {
var use24Hour = (typeof app !== 'undefined' && app.options && app.options.time_format == 24);
if (use24Hour) {
return titleText
.replace(/^(\d{1,2}):(\d{2})([ap])\s*/i, function(match, hour, min, ampm) {
return convertTo24Hour(hour, min, ampm.toLowerCase() === 'p') + ' ';
})
.replace(/^(\d{1,2})([ap])\s*/i, function(match, hour, ampm) {
return convertTo24Hour(hour, '00', ampm.toLowerCase() === 'p') + ' ';
});
} else {
return titleText
.replace(/^(\d{1,2}:\d{2})a\s*/i, '$1 AM ')
.replace(/^(\d{1,2}:\d{2})p\s*/i, '$1 PM ')
.replace(/^(\d{1,2})a\s*/i, '$1:00 AM ')
.replace(/^(\d{1,2})p\s*/i, '$1:00 PM ');
}
}
// Clean up calendar time prefixes
function cleanUpCalendarTimes() {
var selectors = ['.fc-event', '.fc-event-title', '.fc-title'];
var cleaned = 0;
selectors.forEach(function(selector) {
$(selector).each(function() {
var $element = $(this);
var titleText = $element.text();
if (titleText && /^\d{1,2}(:\d{2})?[ap]\s*/i.test(titleText)) {
var cleanTitle = formatTimePrefix(titleText);
if (cleanTitle !== titleText) {
$element.text(cleanTitle);
cleaned++;
}
}
});
});
}
// Initialize cleanup with optimized timing
function initializeCalendarCleanup() {
// Single cleanup after calendar likely renders
setTimeout(cleanUpCalendarTimes, 1000);
// Watch for dynamic content changes
if (typeof MutationObserver !== 'undefined') {
var observer = new MutationObserver(function(mutations) {
var needsCleanup = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
needsCleanup = true;
}
});
if (needsCleanup) {
setTimeout(function() {
cleanUpCalendarTimes();
enhanceCalendarEvents();
}, 100);
}
});
setTimeout(function() {
var calendarEl = document.querySelector('#calendar');
if (calendarEl) {
observer.observe(calendarEl, { childList: true, subtree: true });
}
}, 1000);
}
}
function enhanceCalendarEvents() {
// Target appointly events that haven't been processed yet
$('.fc-event[href*="appointly/appointments/view"]:not(.appointly-event)').each(function() {
var $event = $(this);
var href = $event.attr('href') || '';
var title = $event.text() || '';
// Try multiple ways to get FullCalendar event data
var eventData = $event.data('event') ||
$event.data('fc-event') ||
$event.data('fcEvent') ||
{};
// Skip if this is not a real appointment event
if (!href || href === '#' || href.indexOf('appointly') === -1) {
return;
}
// Only process Appointly events
if (!isAppointlyEvent(href, title, eventData)) {
return;
}
setupTooltip($event, title, href, eventData);
});
}
function isAppointlyEvent(href, title, eventData) {
// Must have a valid appointly URL
if (href.indexOf('appointly/appointments/view') === -1) {
return false;
}
// Exclude non-Appointly events
var isExcluded = title.indexOf('INV-') !== -1 ||
title.indexOf('EST-') !== -1 ||
href.indexOf('/invoices/') !== -1 ||
href.indexOf('/estimates/') !== -1 ||
href.indexOf('/proposals/') !== -1 ||
href.indexOf('/tasks/') !== -1;
if (isExcluded) {
return false;
}
return true;
}
function setupTooltip($event, title, href, eventData) {
// Remove any existing event handlers to prevent duplicates
$event.off('mouseenter.appointly mouseleave.appointly');
// PREVENT OTHER TOOLTIP SYSTEMS FROM INTERFERING
$event.removeAttr('title'); // Remove title attribute that causes browser tooltips
$event.removeAttr('data-original-title'); // Remove Bootstrap tooltip titles
$event.removeAttr('data-toggle'); // Remove Bootstrap tooltip triggers
$event.removeAttr('data-placement'); // Remove Bootstrap tooltip placement
$event.addClass('appointly-event');
// Apply service colors from appointment data
var appointmentId = extractAppointmentId(href);
if (appointmentId) {
fetchAppointmentData(appointmentId, function(data) {
if (data && data.service_color) {
$event.css({
'background-color': data.service_color + ' !important',
'border-left-color': data.service_color + ' !important',
'border-color': data.service_color + ' !important'
});
}
});
}
var $tooltip = null;
$event.on('mouseenter.appointly', function(e) {
// STOP ALL EVENT PROPAGATION to prevent other tooltip systems
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
// Remove any existing tooltips first
$('.appointly-custom-tooltip').remove();
var appointlyData = eventData.appointlyData || {};
var appointmentId = extractAppointmentId(href);
// Fetch data if needed, otherwise create tooltip directly
if (appointmentId && (!appointlyData || Object.keys(appointlyData).length === 0)) {
fetchAppointmentData(appointmentId, function(data) {
createTooltip(data || {}, title, href, appointmentId, $event);
});
} else {
createTooltip(appointlyData, title, href, appointmentId, $event);
}
});
$event.on('mouseleave.appointly', function(e) {
e.stopPropagation();
e.stopImmediatePropagation();
setTimeout(function() {
if ($tooltip && !$tooltip.data('hovered')) {
$tooltip.remove();
}
}, 200);
});
// DISABLE OTHER TOOLTIP EVENTS
$event.on('mouseover mouseout hover', function(e) {
if (!e.target.closest('.appointly-custom-tooltip')) {
e.stopPropagation();
e.stopImmediatePropagation();
}
});
function fetchAppointmentData(appointmentId, callback) {
$.ajax({
url: admin_url + 'appointly/appointments/get_appointment_data',
type: 'POST',
data: { appointment_id: appointmentId },
dataType: 'json',
success: function(response) {
callback(response && response.success ? response.data : null);
},
error: function() {
callback(null);
}
});
}
function createTooltip(appointlyData, title, href, id, $event) {
// Create tooltip container with unique class
$tooltip = $('<div>')
.addClass('appointly-custom-tooltip')
.css(getTooltipBaseStyles());
// Create and append header
var $header = createHeader(title);
$tooltip.append($header);
// Create and append content
if (appointlyData && Object.keys(appointlyData).length > 0) {
appendRichContent($tooltip, appointlyData);
} else {
appendBasicContent($tooltip, id);
}
// Create and append footer
if (href) {
var $footer = createFooter(href);
$tooltip.append($footer);
}
positionTooltip($tooltip, $event);
setupTooltipEvents($tooltip);
}
}
function extractAppointmentId(href) {
var match = href.match(/appointment_id=(\d+)/);
return match ? match[1] : '';
}
function getTooltipBaseStyles() {
return {
background: 'white',
border: '1px solid #ddd',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
maxWidth: '400px',
minWidth: '300px',
zIndex: 10000,
position: 'absolute',
pointerEvents: 'auto'
};
}
function createHeader(title) {
var $header = $('<div>').css({
borderBottom: '2px solid #f8f9fa',
paddingBottom: '12px',
marginBottom: '16px'
});
var $titleEl = $('<h5>').css({
margin: 0,
color: '#2c3e50',
fontSize: '18px',
fontWeight: '700',
lineHeight: '1.3'
});
// Use text() to safely set the title
$titleEl.text(title);
$header.append($titleEl);
return $header;
}
function appendRichContent($tooltip, data) {
// Service information
if (data.service_name) {
$tooltip.append(createInfoRow('fa-briefcase', data.service_name, '#007bff', '600'));
}
// Time information
if (data.start_hour && data.end_hour) {
var timeText = formatTime(data.start_hour) + ' - ' + formatTime(data.end_hour);
if (data.duration || data.service_duration) {
timeText += ' (' + (data.duration || data.service_duration) + ' min)';
}
$tooltip.append(createInfoRow('fa-clock-o', timeText, '#007bff', '500'));
}
// Provider information
if (data.provider_firstname && data.provider_lastname) {
$tooltip.append(createInfoRow('fa-user', data.provider_firstname + ' ' + data.provider_lastname, '#007bff', '500'));
}
// Status badge
if (data.status) {
$tooltip.append(createStatusBadge(data.status));
}
// Contact information
if (data.email) {
$tooltip.append(createInfoRow('fa-envelope', data.email, '#6c757d', '400', '13px'));
}
if (data.phone) {
$tooltip.append(createInfoRow('fa-phone', data.phone, '#6c757d', '400', '13px'));
}
// Address
if (data.address) {
$tooltip.append(createInfoRow('fa-map-marker', data.address, '#6c757d', '400', '13px'));
}
}
function appendBasicContent($tooltip, id) {
if (id) {
$tooltip.append(createInfoRow('fa-calendar', 'Appointment ID: ' + id, '#007bff'));
}
}
function createInfoRow(iconClass, text, color, fontWeight, fontSize) {
color = color || '#666';
fontWeight = fontWeight || '400';
fontSize = fontSize || '14px';
var $row = $('<div>').css({
display: 'flex',
alignItems: 'center',
marginBottom: '10px',
fontSize: fontSize,
color: color,
fontWeight: fontWeight
});
var $icon = $('<i>').addClass('fa ' + iconClass).css({
width: '20px',
marginRight: '12px',
color: '#007bff',
fontSize: '16px'
});
var $textSpan = $('<span>');
$textSpan.text(text); // Use text() method to prevent HTML injection
$row.append($icon);
$row.append($textSpan);
return $row;
}
function createStatusBadge(status) {
var $statusDiv = $('<div>').css({
margin: '12px 0',
textAlign: 'center'
});
var $status = $('<span>').css({
fontSize: '12px',
padding: '6px 12px',
borderRadius: '20px',
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: '0.5px',
background: getStatusColor(status),
color: 'white'
});
// Use text() to safely set status text
$status.text(status.charAt(0).toUpperCase() + status.slice(1));
$statusDiv.append($status);
return $statusDiv;
}
function createFooter(href) {
var $footer = $('<div>').css({
borderTop: '2px solid #f8f9fa',
paddingTop: '16px',
marginTop: '16px',
textAlign: 'center'
});
var $button = $('<a>').attr('href', href).css({
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
padding: '10px 20px',
borderRadius: '8px',
textDecoration: 'none',
background: 'linear-gradient(135deg, #007bff 0%, #0056b3 100%)',
border: 'none',
color: '#fff',
fontWeight: '600',
boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
transition: 'all 0.3s ease'
});
var $buttonIcon = $('<i>').addClass('fa fa-eye').css({ fontSize: '14px' });
var $buttonText = $('<span>');
$buttonText.text(' View Details'); // Use text() method
$button.append($buttonIcon);
$button.append($buttonText);
$footer.append($button);
return $footer;
}
function positionTooltip($tooltip, $event) {
$tooltip.appendTo('body');
var offset = $event.offset();
var tooltipHeight = $tooltip.outerHeight();
var tooltipWidth = $tooltip.outerWidth();
var eventWidth = $event.outerWidth();
// Position above the event, centered
var top = offset.top - tooltipHeight - 10;
var left = offset.left + (eventWidth / 2) - (tooltipWidth / 2);
// Keep tooltip on screen
if (left < 10) left = 10;
if (left + tooltipWidth > $(window).width() - 10) {
left = $(window).width() - tooltipWidth - 10;
}
if (top < 10) {
top = offset.top + $event.outerHeight() + 10;
}
$tooltip.css({ top: top, left: left });
}
function setupTooltipEvents($tooltip) {
$tooltip.on('mouseenter', function() {
$tooltip.data('hovered', true);
});
$tooltip.on('mouseleave', function() {
$tooltip.remove();
});
}
// Utility functions
function formatTime(timeStr) {
if (!timeStr) return '';
var parts = timeStr.split(':');
if (parts.length >= 2) {
var hour = parseInt(parts[0], 10);
var minute = parts[1];
// Check if we should use 24-hour format based on app settings
var use24Hour = false;
if (typeof app !== 'undefined' && app.options && app.options.time_format == 24) {
use24Hour = true;
}
if (use24Hour) {
// 24-hour format
return (hour < 10 ? '0' : '') + hour + ':' + minute;
} else {
// 12-hour format with AM/PM
var ampm = hour >= 12 ? 'PM' : 'AM';
hour = hour % 12;
hour = hour ? hour : 12;
return hour + ':' + minute + ' ' + ampm;
}
}
return timeStr;
}
function getStatusColor(status) {
switch (status.toLowerCase()) {
case 'approved':
case 'confirmed':
case 'in-progress':
return '#28a745';
case 'pending':
return '#ffc107';
case 'cancelled':
case 'no-show':
return '#dc3545';
case 'completed':
return '#17a2b8';
default:
return '#6c757d';
}
}
})(jQuery);