assest pull ever
i should have thought
This commit is contained in:
1 parent
1a2582a8a8
commit
50a90db0b2
9 files changed
+218
-200
No files matched your search
+67
-69
@@ -1,85 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>cal app</title>
|
||||
<link rel="stylesheet" href="/src/style.css" />
|
||||
<script type="module" src="/src/main.js" defer></script>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<header class="text-center mb-4">
|
||||
<h1 class="display-4">🗓️ Calendar Filter App</h1>
|
||||
<p class="lead">Load, filter, and combine calendars with style.</p>
|
||||
</header>
|
||||
<header class="text-center mb-4">
|
||||
<h1 class="display-4">🗓️ Calendar Filter App</h1>
|
||||
<p class="lead">Load, filter, and combine calendars with style.</p>
|
||||
</header>
|
||||
|
||||
<form class="row g-3" onsubmit="return false;">
|
||||
<div class="col-12">
|
||||
<label for="url_mode" class="form-label">Select Mode</label>
|
||||
<select id="url_mode" class="form-select">
|
||||
<option value="manual">Manual URL</option>
|
||||
<option value="args" id="proxy_opt">Send Arguments to Proxy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="manual-url-input" class="col-12">
|
||||
<label for="ics_file" class="form-label">ICS URL</label>
|
||||
<input type="text" id="ics_file" class="form-control"
|
||||
placeholder="Paste .ics URL or API endpoint here" />
|
||||
</div>
|
||||
|
||||
<div id="args-form" class="col-12" style="display: none">
|
||||
<label for="be_url" class="form-label">Calendar URL</label>
|
||||
<input type="text" id="be_url" class="form-control mb-2" placeholder="Enter .ics URL to query" />
|
||||
|
||||
<label for="regex_exc" class="form-label">Regex Exclude Rules</label>
|
||||
<input type="text" id="regex_exc" class="form-control mb-2"
|
||||
placeholder="Enter regex to exclude events" />
|
||||
|
||||
<label for="regex_inc" class="form-label">Regex Include Rules</label>
|
||||
<input type="text" id="regex_inc" class="form-control mb-2"
|
||||
placeholder="Enter regex to include events" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-primary" id="ics_run">
|
||||
New Calendar
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="ics_add">
|
||||
Add a Calendar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="placeholder-message" class="alert alert-warning mt-4">
|
||||
No calendar loaded yet. Paste a URL to get started!
|
||||
<form class="row g-3" onsubmit="return false;">
|
||||
<div class="col-auto">
|
||||
<label for="url_mode" class="form-label">Select Mode</label>
|
||||
<select id="url_mode" class="form-select">
|
||||
<option value="manual">Manual URL</option>
|
||||
<option value="args">Send Arguments to Proxy</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="event-source-management" class="card mt-4" style="display: none">
|
||||
<div class="card-header mt-1 d-flex justify-content-between align-items-center bg-transparent border-0">
|
||||
<h4 class="mb-0">Event Sources</h4>
|
||||
<button id="reload" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reload
|
||||
</button>
|
||||
</div>
|
||||
<div id="event-source-list" class="card-body">
|
||||
<!-- Event source cards will be dynamically added here -->
|
||||
</div>
|
||||
<div id="manual-url-input" class="col">
|
||||
<label for="ics_file" class="form-label">ICS URL</label>
|
||||
<input type="text" id="ics_file" class="form-control" placeholder="Paste .ics URL or API endpoint here" />
|
||||
</div>
|
||||
|
||||
<div id="calendar" class="mt-4"></div>
|
||||
<div id="args-form" class="col-12" style="display: none;">
|
||||
<label for="be_url" class="form-label">Calendar URL</label>
|
||||
<input type="text" id="be_url" class="form-control mb-2" placeholder="Enter .ics URL to query" />
|
||||
|
||||
<footer class="text-center mt-4">
|
||||
<p>Made with Rust, TS, and ❤️</p>
|
||||
<p>
|
||||
by
|
||||
<a href="//ldlda.com" target="_blank" rel="noopener noreferrer">lđa</a>
|
||||
</p>
|
||||
</footer>
|
||||
<label for="regex_exc" class="form-label">Regex Exclude Rules</label>
|
||||
<input type="text" id="regex_exc" class="form-control mb-2" placeholder="Enter regex to exclude events" />
|
||||
|
||||
<label for="regex_inc" class="form-label">Regex Include Rules</label>
|
||||
<input type="text" id="regex_inc" class="form-control mb-2" placeholder="Enter regex to include events" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex justify-content-end flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-primary" id="ics_run">New Calendar</button>
|
||||
<button type="button" class="btn btn-secondary" id="ics_add">Add a Calendar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="placeholder-message" class="alert alert-warning mt-4">
|
||||
No calendar loaded yet. Paste a URL to get started!
|
||||
</div>
|
||||
|
||||
<div id="event-source-management" class="card mt-4" style="display: none">
|
||||
<div
|
||||
class="card-header mt-1 d-flex justify-content-between align-items-center bg-transparent border-0"
|
||||
>
|
||||
<h4 class="mb-0">Event Sources</h4>
|
||||
<button id="reload" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reload
|
||||
</button>
|
||||
</div>
|
||||
<div id="event-source-list" class="card-body">
|
||||
<!-- Event source cards will be dynamically added here -->
|
||||
<div id="placeholder-message" class="alert alert-warning">
|
||||
no source
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="calendar" class="mt-4"></div>
|
||||
|
||||
<footer class="text-center mt-4">
|
||||
<p>Made with Rust, TS, and ❤️</p>
|
||||
<p>
|
||||
by
|
||||
<a href="//ldlda.com" target="_blank" rel="noopener noreferrer"
|
||||
>lđa</a
|
||||
>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
+12
-10
@@ -1,17 +1,17 @@
|
||||
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
|
||||
import {
|
||||
Calendar,
|
||||
EventHoveringArg,
|
||||
EventSourceInput,
|
||||
} from "@fullcalendar/core";
|
||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||
import rrulePlugin from "@fullcalendar/rrule";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import rrulePlugin from "@fullcalendar/rrule";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import bootstrap5Plugin from "@fullcalendar/bootstrap5";
|
||||
|
||||
import { getNextColor } from "./color_generator";
|
||||
import { updateEventSourceList } from "./main";
|
||||
import { loading_callback } from "./main";
|
||||
|
||||
let calendar: Calendar | null = null;
|
||||
|
||||
@@ -61,7 +61,11 @@ export function initializeCalendar(
|
||||
events: init_ics_source(url.toString()),
|
||||
eventMouseEnter: onEventMouseEnter,
|
||||
eventMouseLeave: onEventMouseLeave,
|
||||
loading: () => calendar && updateEventSourceList(calendar),
|
||||
// loading: () => calendar && updateEventSourceList(calendar),
|
||||
loading: (isloading) => {
|
||||
calendar?.getEventSources().forEach(console.log);
|
||||
loading_callback(isloading);
|
||||
},
|
||||
});
|
||||
calendar.render();
|
||||
return calendar; // its real now
|
||||
@@ -71,10 +75,8 @@ export function initializeCalendar(
|
||||
* Adds a new event source to the calendar.
|
||||
* @param url - The URL for the event source.
|
||||
*/
|
||||
export const addIcsEventSource = (url: string): void => {
|
||||
if (calendar) {
|
||||
calendar.addEventSource(init_ics_source(url));
|
||||
}
|
||||
export const addIcsEventSource = (url: string) => {
|
||||
return calendar?.addEventSource(init_ics_source(url));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Start with a random hue, but keep saturation and lightness fixed for "nice" colors
|
||||
let sourceCount = Math.floor(Math.random() * 25);
|
||||
let sourceCount = Math.floor(Math.random() * 360);
|
||||
|
||||
/**
|
||||
* Generates the next color in a sequence using the golden angle.
|
||||
|
||||
+97
-81
@@ -1,85 +1,101 @@
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import { Calendar, EventSourceApi } from "@fullcalendar/core";
|
||||
import { loading_callback } from "./main";
|
||||
|
||||
/**
|
||||
* Updates the event source list in the DOM.
|
||||
* @param eventSourceList - The DOM element to display the event sources.
|
||||
* @returns A function that updates the event source list for a given calendar.
|
||||
*/
|
||||
export const updateEventSourceList2 = (eventSourceList: HTMLElement | null) => {
|
||||
return function updateEventSourceList(calendar: Calendar): void {
|
||||
if (!eventSourceList) return;
|
||||
if (!calendar) return;
|
||||
export interface InternalEventSource {
|
||||
isFetching: boolean;
|
||||
ui: { borderColor?: string };
|
||||
}
|
||||
|
||||
// Clear the current list
|
||||
export interface CustomEventSource extends EventSourceApi {
|
||||
internalEventSource: InternalEventSource;
|
||||
}
|
||||
|
||||
// interface loadingIndicatorCard extends HTMLDivElement {
|
||||
// refreshLoading: Function;
|
||||
// }
|
||||
|
||||
export function addEventSourceCard(
|
||||
eventSourceList: HTMLElement,
|
||||
source: CustomEventSource
|
||||
) {
|
||||
const internalEventSource = source.internalEventSource;
|
||||
const card = document.createElement("div");
|
||||
const color = internalEventSource.ui.borderColor || "#007bff";
|
||||
card.className = "card mb-3";
|
||||
card.style.borderLeft = `4px solid ${color}`;
|
||||
card.dataset.sourceUrl = source.url;
|
||||
|
||||
const cardBody = document.createElement("div");
|
||||
cardBody.className = "card-body";
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "mb-2";
|
||||
const url = source.url || "(No URL)";
|
||||
const format = source.format || "(Unknown Format)";
|
||||
info.innerHTML = `
|
||||
<div><strong>Source:</strong> <a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></div>
|
||||
<div><strong>Format:</strong> <span>${format}</span></div>
|
||||
`;
|
||||
|
||||
const loadingStatus = document.createElement("div");
|
||||
loadingStatus.className = "text-muted small flex-grow-1";
|
||||
updateLoadingStatus(loadingStatus, source);
|
||||
|
||||
const refetchButton = document.createElement("button");
|
||||
refetchButton.textContent = "Refetch";
|
||||
refetchButton.className = "btn btn-sm btn-primary";
|
||||
refetchButton.addEventListener("click", () => {
|
||||
source.refetch();
|
||||
});
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.textContent = "Remove";
|
||||
removeButton.className = "btn btn-sm btn-danger";
|
||||
removeButton.addEventListener("click", () => {
|
||||
source.remove();
|
||||
loading_callback(false);
|
||||
});
|
||||
|
||||
const actionContainer = document.createElement("div");
|
||||
actionContainer.className = "d-flex justify-content-between flex-wrap";
|
||||
actionContainer.appendChild(loadingStatus);
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.className = "d-flex align-items-center gap-1 flex-wrap";
|
||||
buttonContainer.appendChild(refetchButton);
|
||||
buttonContainer.appendChild(removeButton);
|
||||
actionContainer.appendChild(buttonContainer);
|
||||
cardBody.appendChild(info);
|
||||
cardBody.appendChild(actionContainer);
|
||||
card.appendChild(cardBody);
|
||||
eventSourceList.appendChild(card);
|
||||
}
|
||||
|
||||
export function updateLoadingStatus(
|
||||
loadingStatus: HTMLElement,
|
||||
source: CustomEventSource
|
||||
) {
|
||||
loadingStatus.textContent = source.internalEventSource.isFetching
|
||||
? "Fetching in progress"
|
||||
: "Loaded";
|
||||
}
|
||||
|
||||
export const rerender_all_statuses = (
|
||||
eventSourceList: HTMLElement,
|
||||
calendar: Calendar | null
|
||||
) =>
|
||||
setTimeout(() => {
|
||||
eventSourceList.innerHTML = "";
|
||||
|
||||
// Fetch all event sources
|
||||
calendar.getEventSources().forEach((source) => {
|
||||
console.log(source);
|
||||
const card = document.createElement("div");
|
||||
const color = source.internalEventSource.ui.borderColor || "#007bff"; // Default to Bootstrap primary color
|
||||
card.className = "card mb-3";
|
||||
card.style.borderLeft = `4px solid ${color}`;
|
||||
|
||||
// cardBody serves as a container for all card content
|
||||
const cardBody = document.createElement("div");
|
||||
cardBody.className = "card-body";
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "mb-2";
|
||||
const url = source.url || "(No URL)";
|
||||
const format = source.format || "(Unknown Format)";
|
||||
info.innerHTML = `
|
||||
<div>
|
||||
<strong>Source:</strong>
|
||||
<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>
|
||||
</div>
|
||||
<div><strong>Format:</strong> <span>${format}</span></div>
|
||||
`;
|
||||
|
||||
const loadingStatus = document.createElement("div");
|
||||
loadingStatus.className = "text-muted small flex-grow-1";
|
||||
loadingStatus.textContent = source.internalEventSource.isFetching
|
||||
? "Fetching in progress"
|
||||
: "Loaded";
|
||||
|
||||
// Add Refetch Button
|
||||
const refetchButton = document.createElement("button");
|
||||
refetchButton.textContent = "Refetch";
|
||||
refetchButton.className = "btn btn-sm btn-primary";
|
||||
refetchButton.addEventListener("click", () => {
|
||||
source.refetch();
|
||||
});
|
||||
|
||||
// Add Remove Button
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.textContent = "Remove";
|
||||
removeButton.className = "btn btn-sm btn-danger";
|
||||
removeButton.addEventListener("click", () => {
|
||||
source.remove();
|
||||
updateEventSourceList(calendar); // Refresh the list after removal
|
||||
});
|
||||
|
||||
// Create a container for loading status and buttons
|
||||
const actionContainer = document.createElement("div");
|
||||
actionContainer.className = "d-flex align-items-center gap-2 flex-wrap";
|
||||
|
||||
// Add loading status to the container
|
||||
actionContainer.appendChild(loadingStatus);
|
||||
|
||||
// Add buttons to the container
|
||||
actionContainer.appendChild(refetchButton);
|
||||
actionContainer.appendChild(removeButton);
|
||||
|
||||
// Append info and action container to the card body
|
||||
cardBody.appendChild(info);
|
||||
cardBody.appendChild(actionContainer);
|
||||
|
||||
// Append the card body to the card
|
||||
card.appendChild(cardBody);
|
||||
|
||||
// Append the card to the event source list
|
||||
eventSourceList.appendChild(card);
|
||||
calendar?.getEventSources().forEach((eventSource) => {
|
||||
addEventSourceCard(eventSourceList, eventSource);
|
||||
});
|
||||
};
|
||||
};
|
||||
const parent = eventSourceList.parentElement;
|
||||
if (parent)
|
||||
!eventSourceList.innerHTML
|
||||
? (parent.style.display = "none")
|
||||
: (parent.style.display = "");
|
||||
}) && undefined;
|
||||
|
||||
// eventSourceList.innerHTML = `
|
||||
// <div id="placeholder-message" class="alert alert-warning">
|
||||
// no source
|
||||
// </div>`;
|
||||
+13
-3
@@ -13,9 +13,9 @@ export function getFormParams(): Record<string, string> {
|
||||
return params;
|
||||
}
|
||||
|
||||
export function toggleFormVisibility(mode: string): void {
|
||||
const manualUrlInput = getElementById<HTMLElement>("manual-url-input");
|
||||
const argsForm = getElementById<HTMLElement>("args-form");
|
||||
const manualUrlInput = getElementById<HTMLElement>("manual-url-input");
|
||||
const argsForm = getElementById<HTMLElement>("args-form");
|
||||
export function handle_form_change(mode: string): void {
|
||||
|
||||
if (mode === "manual") {
|
||||
manualUrlInput.style.display = "block";
|
||||
@@ -27,4 +27,14 @@ export function toggleFormVisibility(mode: string): void {
|
||||
manualUrlInput.style.display = "block";
|
||||
argsForm.style.display = "none";
|
||||
}
|
||||
|
||||
// // Re-enable all form elements
|
||||
// manualUrlInput.querySelectorAll("input, select, button").forEach((el) => {
|
||||
// (el as HTMLInputElement | HTMLSelectElement | HTMLButtonElement).disabled =
|
||||
// false;
|
||||
// });
|
||||
// argsForm.querySelectorAll("input, select, button").forEach((el) => {
|
||||
// (el as HTMLInputElement | HTMLSelectElement | HTMLButtonElement).disabled =
|
||||
// false;
|
||||
// });
|
||||
}
|
||||
+22
-28
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
initializeCalendar,
|
||||
addIcsEventSource,
|
||||
getCalendarInstance,
|
||||
initializeCalendar,
|
||||
} from "./calendar";
|
||||
import { newTippy, tippyDie } from "./tooltip";
|
||||
import { updateEventSourceList2 } from "./eventSourceManager";
|
||||
import { getElementById } from "./utils";
|
||||
import { rerender_all_statuses } from "./eventSourceManager";
|
||||
import { getFormParams, handle_form_change } from "./formHandler";
|
||||
import { manageUrl } from "./queryHandler";
|
||||
import { getFormParams, toggleFormVisibility } from "./formHandler";
|
||||
import { newTippy, tippyDie } from "./tooltip";
|
||||
import { getElementById } from "./utils";
|
||||
|
||||
const text_field = getElementById<HTMLInputElement>("ics_file");
|
||||
const confirm_button = getElementById<HTMLButtonElement>("ics_run");
|
||||
@@ -18,13 +18,9 @@ const eventSourceList = getElementById<HTMLElement>("event-source-list");
|
||||
const eventmanage = getElementById<HTMLElement>("event-source-management");
|
||||
const reload_button = getElementById<HTMLButtonElement>("reload");
|
||||
const urlMode = getElementById<HTMLSelectElement>("url_mode");
|
||||
const proxy_option = getElementById<HTMLSelectElement>("proxy_opt");
|
||||
|
||||
export const updateEventSourceList = updateEventSourceList2(eventSourceList);
|
||||
export const baseUrl = "http://127.0.0.1:8080/filtered-ics";
|
||||
|
||||
urlMode.addEventListener("change", () => {
|
||||
toggleFormVisibility(urlMode.value);
|
||||
handle_form_change(urlMode.value);
|
||||
});
|
||||
|
||||
function handleConfirmClick(): void {
|
||||
@@ -33,14 +29,12 @@ function handleConfirmClick(): void {
|
||||
return alert("Please input a valid URL.");
|
||||
}
|
||||
|
||||
{
|
||||
placeholder_message.style.display = "none";
|
||||
eventmanage.style.display = "block";
|
||||
cal_ele.style.display = "block";
|
||||
}
|
||||
placeholder_message.style.display = "none";
|
||||
eventmanage.style.display = "block";
|
||||
cal_ele.style.display = "block";
|
||||
|
||||
const calendar = initializeCalendar(cal_ele, url, newTippy, tippyDie);
|
||||
updateEventSourceList(calendar);
|
||||
rerender_all_statuses(eventSourceList, calendar);
|
||||
}
|
||||
|
||||
function handleAddClick(): void {
|
||||
@@ -52,27 +46,27 @@ function handleAddClick(): void {
|
||||
const calendar = getCalendarInstance();
|
||||
if (calendar) {
|
||||
addIcsEventSource(url);
|
||||
updateEventSourceList(calendar);
|
||||
rerender_all_statuses(eventSourceList, calendar);
|
||||
}
|
||||
}
|
||||
|
||||
function fetch_url_form() {
|
||||
const params = getFormParams();
|
||||
const url = manageUrl(urlMode.value, text_field.value, baseUrl, params);
|
||||
const baseUrl = text_field.value || "http://127.0.0.1:8080/filtered-ics";
|
||||
const url = manageUrl(urlMode.value, baseUrl, params);
|
||||
return url;
|
||||
}
|
||||
|
||||
function handleReloadClick(): void {
|
||||
const calendar = getCalendarInstance();
|
||||
if (calendar) {
|
||||
updateEventSourceList(calendar);
|
||||
}
|
||||
rerender_all_statuses(eventSourceList, calendar);
|
||||
}
|
||||
|
||||
// running
|
||||
{
|
||||
proxy_option.textContent += ` (currently "${baseUrl}")`;
|
||||
confirm_button.addEventListener("click", handleConfirmClick);
|
||||
add_button.addEventListener("click", handleAddClick);
|
||||
reload_button.addEventListener("click", handleReloadClick);
|
||||
}
|
||||
export const loading_callback = (_: unknown) => {
|
||||
const calendar = getCalendarInstance();
|
||||
rerender_all_statuses(eventSourceList, calendar);
|
||||
};
|
||||
|
||||
confirm_button.addEventListener("click", handleConfirmClick);
|
||||
add_button.addEventListener("click", handleAddClick);
|
||||
reload_button.addEventListener("click", handleReloadClick);
|
||||
+4
-3
@@ -22,14 +22,15 @@ export function queryCustomUrl(
|
||||
|
||||
export function manageUrl(
|
||||
mode: string,
|
||||
manualInput: string,
|
||||
base_url: string,
|
||||
params: Record<string, string>
|
||||
): string | null {
|
||||
if (mode === "manual") {
|
||||
return create_url(manualInput);
|
||||
const url = create_url(base_url);
|
||||
return url;
|
||||
} else if (mode === "args") {
|
||||
return constructUrl(base_url, params);
|
||||
const url = constructUrl(base_url, params);
|
||||
return url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
+1
-4
@@ -8,10 +8,7 @@
|
||||
@import "tippy.js/dist/tippy.css";
|
||||
|
||||
body {
|
||||
/* font-family: system-ui, sans-serif; */
|
||||
background: #f8f9fa !important; /* cant be used since overriden */
|
||||
/* margin: 0;
|
||||
padding: 0; */
|
||||
background: #f8f9fa !important; /* override bootstrap body */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { EventHoveringArg } from "@fullcalendar/core";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import { formatTooltip } from "./tooltip_parser";
|
||||
import { EventHoveringArg } from "@fullcalendar/core";
|
||||
|
||||
let currentTippy: Instance<Props> | null = null;
|
||||
|
||||
|
||||
Reference in new issue
Block a user