mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 04:32:15 +01:00
add support for window activities in the calendar view plugin
This commit is contained in:
@@ -712,28 +712,41 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Activity status plugin styles */
|
/* Activity status plugin styles */
|
||||||
.fullcalendar :deep(.activity-status-box) {
|
.fullcalendar :deep(.activity-status-box) {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullcalendar :deep(.activity-status-box::before) {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 5px;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.activity-status-box.idle) {
|
.fullcalendar :deep(.activity-status-box.idle::before) {
|
||||||
background-color: rgba(156, 163, 175, 0.1) !important;
|
background-color: rgba(156, 163, 175, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.activity-status-box.idle):hover {
|
.fullcalendar :deep(.activity-status-box.idle):hover::before {
|
||||||
background-color: rgba(156, 163, 175, 0.5) !important;
|
background-color: rgba(156, 163, 175, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.activity-status-box.active) {
|
.fullcalendar :deep(.activity-status-box.active::before) {
|
||||||
background-color: rgba(34, 197, 94, 0.3) !important;
|
background-color: rgba(34, 197, 94, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.activity-status-box.active):hover {
|
.fullcalendar :deep(.activity-status-box.active):hover::before {
|
||||||
background-color: rgba(34, 197, 94, 1) !important;
|
background-color: rgba(34, 197, 94, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add left margin to events only on days with activity status data */
|
/* Add left margin to events only on days with activity status data */
|
||||||
.fullcalendar :deep(.has-activity-status .fc-timegrid-event-harness) {
|
.fullcalendar :deep(.has-activity-status .fc-timegrid-event-harness) {
|
||||||
margin-left: 15px !important;
|
margin-left: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullcalendar :deep(.fc-timegrid-event) {
|
.fullcalendar :deep(.fc-timegrid-event) {
|
||||||
|
|||||||
@@ -1,42 +1,70 @@
|
|||||||
import { createPlugin, type PluginDef } from '@fullcalendar/core';
|
import { createPlugin, type PluginDef } from '@fullcalendar/core';
|
||||||
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
|
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom';
|
||||||
|
|
||||||
|
export interface WindowActivityInPeriod {
|
||||||
|
appName: string;
|
||||||
|
url: string | null;
|
||||||
|
count: number;
|
||||||
|
icon?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActivityPeriod {
|
export interface ActivityPeriod {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
isIdle: boolean;
|
isIdle: boolean;
|
||||||
|
windowActivities?: WindowActivityInPeriod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityStatusPluginOptions {
|
export interface ActivityStatusPluginOptions {
|
||||||
activityPeriods?: ActivityPeriod[];
|
activityPeriods?: ActivityPeriod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tooltip state management - single instance per module
|
||||||
|
let tooltipInstance: HTMLElement | null = null;
|
||||||
|
let cleanupAutoUpdate: (() => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and manages a tooltip element for activity status boxes
|
* Creates and manages a tooltip element for activity status boxes
|
||||||
*/
|
*/
|
||||||
function createTooltip(): HTMLElement {
|
function getOrCreateTooltip(): HTMLElement {
|
||||||
const tooltip = document.createElement('div');
|
if (!tooltipInstance) {
|
||||||
tooltip.className =
|
tooltipInstance = document.createElement('div');
|
||||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground';
|
tooltipInstance.className =
|
||||||
tooltip.style.position = 'fixed';
|
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground';
|
||||||
tooltip.style.pointerEvents = 'none';
|
tooltipInstance.style.position = 'fixed';
|
||||||
tooltip.style.opacity = '0';
|
tooltipInstance.style.pointerEvents = 'none';
|
||||||
tooltip.style.whiteSpace = 'nowrap';
|
tooltipInstance.style.opacity = '0';
|
||||||
tooltip.style.transform = 'scale(0.95)';
|
tooltipInstance.style.whiteSpace = 'nowrap';
|
||||||
tooltip.style.transition = 'opacity 150ms, transform 150ms';
|
tooltipInstance.style.transform = 'scale(0.95)';
|
||||||
document.body.appendChild(tooltip);
|
tooltipInstance.style.transition = 'opacity 150ms, transform 150ms';
|
||||||
return tooltip;
|
document.body.appendChild(tooltipInstance);
|
||||||
|
}
|
||||||
|
return tooltipInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows tooltip for an activity status box
|
* Shows tooltip for an activity status box using Floating UI's autoUpdate
|
||||||
*/
|
*/
|
||||||
function showTooltip(box: HTMLElement, tooltip: HTMLElement, text: string) {
|
function showTooltip(box: HTMLElement, tooltip: HTMLElement, content: string | HTMLElement) {
|
||||||
tooltip.textContent = text;
|
// Clear previous content
|
||||||
|
tooltip.innerHTML = '';
|
||||||
|
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
tooltip.textContent = content;
|
||||||
|
} else {
|
||||||
|
tooltip.appendChild(content);
|
||||||
|
}
|
||||||
|
|
||||||
tooltip.style.opacity = '1';
|
tooltip.style.opacity = '1';
|
||||||
tooltip.style.transform = 'scale(1)';
|
tooltip.style.transform = 'scale(1)';
|
||||||
|
|
||||||
const updatePosition = () => {
|
// Clean up previous autoUpdate if it exists
|
||||||
|
if (cleanupAutoUpdate) {
|
||||||
|
cleanupAutoUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use autoUpdate to automatically update position
|
||||||
|
cleanupAutoUpdate = autoUpdate(box, tooltip, () => {
|
||||||
computePosition(box, tooltip, {
|
computePosition(box, tooltip, {
|
||||||
placement: 'right',
|
placement: 'right',
|
||||||
middleware: [offset(8), flip(), shift({ padding: 5 })],
|
middleware: [offset(8), flip(), shift({ padding: 5 })],
|
||||||
@@ -44,17 +72,124 @@ function showTooltip(box: HTMLElement, tooltip: HTMLElement, text: string) {
|
|||||||
tooltip.style.left = `${x}px`;
|
tooltip.style.left = `${x}px`;
|
||||||
tooltip.style.top = `${y}px`;
|
tooltip.style.top = `${y}px`;
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
updatePosition();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the tooltip
|
* Hides the tooltip immediately
|
||||||
*/
|
*/
|
||||||
function hideTooltip(tooltip: HTMLElement) {
|
function hideTooltip(tooltip: HTMLElement) {
|
||||||
tooltip.style.opacity = '0';
|
tooltip.style.opacity = '0';
|
||||||
tooltip.style.transform = 'scale(0.95)';
|
tooltip.style.transform = 'scale(0.95)';
|
||||||
|
|
||||||
|
// Clean up autoUpdate when tooltip is hidden
|
||||||
|
if (cleanupAutoUpdate) {
|
||||||
|
cleanupAutoUpdate();
|
||||||
|
cleanupAutoUpdate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats duration in minutes to human readable format
|
||||||
|
*/
|
||||||
|
function formatDuration(durationMinutes: number): string {
|
||||||
|
const hours = Math.floor(durationMinutes / 60);
|
||||||
|
const minutes = durationMinutes % 60;
|
||||||
|
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates tooltip content for an activity period
|
||||||
|
*/
|
||||||
|
function createTooltipContent(
|
||||||
|
status: string,
|
||||||
|
durationText: string,
|
||||||
|
windowActivities?: WindowActivityInPeriod[]
|
||||||
|
): string | HTMLElement {
|
||||||
|
if (!windowActivities || windowActivities.length === 0) {
|
||||||
|
return `${status} (${durationText})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.maxWidth = '300px';
|
||||||
|
|
||||||
|
// Header with status and duration
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.style.fontWeight = '600';
|
||||||
|
header.style.marginBottom = '8px';
|
||||||
|
header.textContent = `${status} (${durationText})`;
|
||||||
|
container.appendChild(header);
|
||||||
|
|
||||||
|
// Window activities list
|
||||||
|
const totalActivities = windowActivities.reduce((sum, act) => sum + act.count, 0);
|
||||||
|
|
||||||
|
// Show top 5 activities
|
||||||
|
const topActivities = windowActivities.slice(0, 5);
|
||||||
|
|
||||||
|
topActivities.forEach((activity) => {
|
||||||
|
const activityDiv = document.createElement('div');
|
||||||
|
activityDiv.style.marginTop = '4px';
|
||||||
|
activityDiv.style.fontSize = '11px';
|
||||||
|
activityDiv.style.opacity = '0.9';
|
||||||
|
activityDiv.style.display = 'flex';
|
||||||
|
activityDiv.style.alignItems = 'center';
|
||||||
|
activityDiv.style.gap = '6px';
|
||||||
|
|
||||||
|
// Add icon if available
|
||||||
|
if (activity.icon) {
|
||||||
|
const icon = document.createElement('img');
|
||||||
|
icon.src = activity.icon;
|
||||||
|
icon.alt = activity.appName;
|
||||||
|
icon.style.width = '16px';
|
||||||
|
icon.style.height = '16px';
|
||||||
|
icon.style.borderRadius = '2px';
|
||||||
|
icon.style.flexShrink = '0';
|
||||||
|
activityDiv.appendChild(icon);
|
||||||
|
} else {
|
||||||
|
// Placeholder for no icon
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.style.width = '16px';
|
||||||
|
placeholder.style.height = '16px';
|
||||||
|
placeholder.style.borderRadius = '2px';
|
||||||
|
placeholder.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
||||||
|
placeholder.style.display = 'flex';
|
||||||
|
placeholder.style.alignItems = 'center';
|
||||||
|
placeholder.style.justifyContent = 'center';
|
||||||
|
placeholder.style.fontSize = '8px';
|
||||||
|
placeholder.style.flexShrink = '0';
|
||||||
|
placeholder.textContent = activity.appName.charAt(0).toUpperCase();
|
||||||
|
activityDiv.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSpan = document.createElement('span');
|
||||||
|
textSpan.style.flex = '1';
|
||||||
|
textSpan.style.overflow = 'hidden';
|
||||||
|
textSpan.style.textOverflow = 'ellipsis';
|
||||||
|
textSpan.style.whiteSpace = 'nowrap';
|
||||||
|
|
||||||
|
const percentage = ((activity.count / totalActivities) * 100).toFixed(0);
|
||||||
|
const activityText = activity.url
|
||||||
|
? `${activity.appName} - ${activity.url}`
|
||||||
|
: activity.appName;
|
||||||
|
|
||||||
|
textSpan.textContent = `${percentage}% ${activityText}`;
|
||||||
|
activityDiv.appendChild(textSpan);
|
||||||
|
|
||||||
|
container.appendChild(activityDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show "and X more" if there are more activities
|
||||||
|
if (windowActivities.length > 5) {
|
||||||
|
const moreDiv = document.createElement('div');
|
||||||
|
moreDiv.style.marginTop = '4px';
|
||||||
|
moreDiv.style.fontSize = '11px';
|
||||||
|
moreDiv.style.opacity = '0.7';
|
||||||
|
moreDiv.style.fontStyle = 'italic';
|
||||||
|
moreDiv.textContent = `...and ${windowActivities.length - 5} more`;
|
||||||
|
container.appendChild(moreDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,49 +201,32 @@ export function renderActivityStatusBoxes(
|
|||||||
) {
|
) {
|
||||||
if (!calendarEl) return;
|
if (!calendarEl) return;
|
||||||
|
|
||||||
// Clean up existing activity boxes and markers first
|
// Clean up existing activity boxes
|
||||||
const existingBoxes = calendarEl.querySelectorAll('.activity-status-box');
|
const existingBoxes = calendarEl.querySelectorAll('.activity-status-box');
|
||||||
existingBoxes.forEach((box) => box.remove());
|
existingBoxes.forEach((box) => box.remove());
|
||||||
|
|
||||||
// Clean up existing tooltips
|
|
||||||
const existingTooltips = document.querySelectorAll('.activity-status-tooltip');
|
|
||||||
existingTooltips.forEach((tooltip) => tooltip.remove());
|
|
||||||
|
|
||||||
// Remove has-activity-status class from all lanes
|
// Remove has-activity-status class from all lanes
|
||||||
const allLanes = calendarEl.querySelectorAll('.fc-timegrid-col');
|
const allLanes = calendarEl.querySelectorAll('.fc-timegrid-col');
|
||||||
allLanes.forEach((lane) => lane.classList.remove('has-activity-status'));
|
allLanes.forEach((lane) => lane.classList.remove('has-activity-status'));
|
||||||
|
|
||||||
const timeGrid = calendarEl.querySelector('.fc-timegrid-body');
|
const timeGrid = calendarEl.querySelector('.fc-timegrid-body');
|
||||||
if (!timeGrid) {
|
if (!timeGrid) return;
|
||||||
console.log('No timegrid found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lanes = timeGrid.querySelectorAll('.fc-timegrid-col');
|
const lanes = timeGrid.querySelectorAll('.fc-timegrid-col');
|
||||||
if (lanes.length === 0) {
|
if (lanes.length === 0) return;
|
||||||
console.log('No lanes found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
// Get or reuse the single tooltip instance
|
||||||
'Rendering activity status boxes, lanes:',
|
const tooltip = getOrCreateTooltip();
|
||||||
lanes.length,
|
|
||||||
'periods:',
|
|
||||||
activityPeriods.length
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a single tooltip instance to be reused
|
// Get slot duration from calendar (fallback to 15 minutes)
|
||||||
const tooltip = createTooltip();
|
const slotDurationMinutes = getSlotDuration(calendarEl);
|
||||||
|
|
||||||
lanes.forEach((lane: Element, dayIndex: number) => {
|
lanes.forEach((lane: Element) => {
|
||||||
// Get the date for this lane from the data attribute
|
// Get the date for this lane from the data attribute
|
||||||
const laneEl = lane as HTMLElement;
|
const laneEl = lane as HTMLElement;
|
||||||
const dateStr = laneEl.getAttribute('data-date');
|
const dateStr = laneEl.getAttribute('data-date');
|
||||||
|
|
||||||
if (!dateStr) {
|
if (!dateStr) return;
|
||||||
console.log('No date attribute found for lane', dayIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const laneDate = new Date(dateStr);
|
const laneDate = new Date(dateStr);
|
||||||
const laneDateStart = new Date(laneDate);
|
const laneDateStart = new Date(laneDate);
|
||||||
@@ -127,47 +245,46 @@ export function renderActivityStatusBoxes(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the position and height of the idle box
|
// Calculate actual start and end times for this day
|
||||||
|
const actualStart = periodStart > laneDateStart ? periodStart : laneDateStart;
|
||||||
|
const actualEnd = periodEnd < laneDateEnd ? periodEnd : laneDateEnd;
|
||||||
|
|
||||||
|
// Calculate the position and height of the activity box
|
||||||
const { top, height } = calculateBoxPosition(
|
const { top, height } = calculateBoxPosition(
|
||||||
calendarEl,
|
calendarEl,
|
||||||
periodStart > laneDateStart ? periodStart : laneDateStart,
|
actualStart,
|
||||||
periodEnd < laneDateEnd ? periodEnd : laneDateEnd
|
actualEnd,
|
||||||
|
slotDurationMinutes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (height <= 0) return;
|
if (height <= 0) return;
|
||||||
|
|
||||||
hasActivityStatusForThisDay = true;
|
hasActivityStatusForThisDay = true;
|
||||||
|
|
||||||
// Create and append the activity status box
|
|
||||||
const box = document.createElement('div');
|
|
||||||
box.className = `activity-status-box ${period.isIdle ? 'idle' : 'active'}`;
|
|
||||||
box.style.position = 'absolute';
|
|
||||||
box.style.top = `${top}px`;
|
|
||||||
box.style.height = `${height}px`;
|
|
||||||
box.style.width = '8px';
|
|
||||||
box.style.left = '4px';
|
|
||||||
box.style.right = '4px';
|
|
||||||
box.style.zIndex = '10';
|
|
||||||
box.style.cursor = 'default';
|
|
||||||
|
|
||||||
// Calculate duration in minutes
|
// Calculate duration in minutes
|
||||||
const actualStart = periodStart > laneDateStart ? periodStart : laneDateStart;
|
|
||||||
const actualEnd = periodEnd < laneDateEnd ? periodEnd : laneDateEnd;
|
|
||||||
const durationMs = actualEnd.getTime() - actualStart.getTime();
|
const durationMs = actualEnd.getTime() - actualStart.getTime();
|
||||||
const durationMinutes = Math.round(durationMs / 60000);
|
const durationMinutes = Math.round(durationMs / 60000);
|
||||||
|
const durationText = formatDuration(durationMinutes);
|
||||||
// Format duration
|
|
||||||
const hours = Math.floor(durationMinutes / 60);
|
|
||||||
const minutes = durationMinutes % 60;
|
|
||||||
const durationText = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
||||||
|
|
||||||
// Add tooltip text based on status
|
// Add tooltip text based on status
|
||||||
const status = period.isIdle ? 'Idling' : 'Active';
|
const status = period.isIdle ? 'Idling' : 'Active';
|
||||||
const tooltipText = `${status} (${durationText})`;
|
|
||||||
|
// Create and append the activity status box
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.className = `activity-status-box ${period.isIdle ? 'idle' : 'active'}`;
|
||||||
|
box.style.top = `${top}px`;
|
||||||
|
box.style.height = `${height}px`;
|
||||||
|
|
||||||
|
// Store tooltip content generator in data attribute for event delegation
|
||||||
|
const tooltipContent = createTooltipContent(
|
||||||
|
status,
|
||||||
|
durationText,
|
||||||
|
period.windowActivities
|
||||||
|
);
|
||||||
|
|
||||||
// Add hover event listeners for tooltip
|
// Add hover event listeners for tooltip
|
||||||
box.addEventListener('mouseenter', () => {
|
box.addEventListener('mouseenter', () => {
|
||||||
showTooltip(box, tooltip, tooltipText);
|
showTooltip(box, tooltip, tooltipContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
box.addEventListener('mouseleave', () => {
|
box.addEventListener('mouseleave', () => {
|
||||||
@@ -178,8 +295,6 @@ export function renderActivityStatusBoxes(
|
|||||||
const laneFrame = lane.querySelector('.fc-timegrid-col-frame');
|
const laneFrame = lane.querySelector('.fc-timegrid-col-frame');
|
||||||
if (laneFrame) {
|
if (laneFrame) {
|
||||||
laneFrame.appendChild(box);
|
laneFrame.appendChild(box);
|
||||||
} else {
|
|
||||||
console.log('No lane frame found');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,18 +305,43 @@ export function renderActivityStatusBoxes(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the slot duration from the calendar configuration
|
||||||
|
*/
|
||||||
|
function getSlotDuration(calendarEl: HTMLElement): number {
|
||||||
|
const slotsEl = calendarEl.querySelectorAll('.fc-timegrid-slot');
|
||||||
|
if (slotsEl.length < 2) return 15; // Default to 15 minutes
|
||||||
|
|
||||||
|
// Try to calculate from the time difference between slots
|
||||||
|
const firstSlot = slotsEl[0] as HTMLElement;
|
||||||
|
const secondSlot = slotsEl[1] as HTMLElement;
|
||||||
|
|
||||||
|
const firstTime = firstSlot.getAttribute('data-time');
|
||||||
|
const secondTime = secondSlot.getAttribute('data-time');
|
||||||
|
|
||||||
|
if (firstTime && secondTime) {
|
||||||
|
const [h1, m1] = firstTime.split(':').map(Number);
|
||||||
|
const [h2, m2] = secondTime.split(':').map(Number);
|
||||||
|
const diff = h2 * 60 + m2 - (h1 * 60 + m1);
|
||||||
|
if (diff > 0) return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to 15 minutes
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the pixel position and height for an activity status box
|
* Calculates the pixel position and height for an activity status box
|
||||||
*/
|
*/
|
||||||
function calculateBoxPosition(
|
function calculateBoxPosition(
|
||||||
calendarEl: HTMLElement,
|
calendarEl: HTMLElement,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
endTime: Date
|
endTime: Date,
|
||||||
|
slotDurationMinutes: number
|
||||||
): { top: number; height: number } {
|
): { top: number; height: number } {
|
||||||
// Get the slot duration and slot height
|
// Get the slot duration and slot height
|
||||||
const slotsEl = calendarEl.querySelectorAll('.fc-timegrid-slot');
|
const slotsEl = calendarEl.querySelectorAll('.fc-timegrid-slot');
|
||||||
if (slotsEl.length === 0) {
|
if (slotsEl.length === 0) {
|
||||||
console.log('No slots found');
|
|
||||||
return { top: 0, height: 0 };
|
return { top: 0, height: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +349,6 @@ function calculateBoxPosition(
|
|||||||
const firstSlot = slotsEl[0] as HTMLElement;
|
const firstSlot = slotsEl[0] as HTMLElement;
|
||||||
const slotHeight = firstSlot.offsetHeight;
|
const slotHeight = firstSlot.offsetHeight;
|
||||||
|
|
||||||
// Each slot is 15 minutes by default (configured in TimeEntryCalendar)
|
|
||||||
const slotDurationMinutes = 15;
|
|
||||||
const pixelsPerMinute = slotHeight / slotDurationMinutes;
|
const pixelsPerMinute = slotHeight / slotDurationMinutes;
|
||||||
|
|
||||||
// Calculate start position (minutes from midnight)
|
// Calculate start position (minutes from midnight)
|
||||||
@@ -224,6 +362,20 @@ function calculateBoxPosition(
|
|||||||
return { top, height };
|
return { top, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup function to remove tooltip from DOM
|
||||||
|
*/
|
||||||
|
export function cleanupActivityStatusPlugin() {
|
||||||
|
if (tooltipInstance) {
|
||||||
|
tooltipInstance.remove();
|
||||||
|
tooltipInstance = null;
|
||||||
|
}
|
||||||
|
if (cleanupAutoUpdate) {
|
||||||
|
cleanupAutoUpdate();
|
||||||
|
cleanupAutoUpdate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FullCalendar plugin to display idle/active status boxes in the time grid
|
* FullCalendar plugin to display idle/active status boxes in the time grid
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user