feat: update language support and enhance API functionality
- Changed the default language in `index.html` from Russian to English, updating the title and button aria-labels for improved accessibility. - Refactored the `buildFetchOptions` function in `api.js` to include an optional external abort signal, enhancing request management. - Updated `fetchDuties` and `fetchCalendarEvents` to support request cancellation using the new abort signal, improving error handling. - Added unit tests for the API functions to ensure proper functionality, including handling of 403 errors and request cancellations. - Enhanced CSS styles for duty markers to improve visual consistency. - Removed unused code and improved the overall structure of the JavaScript files for better maintainability.
This commit is contained in:
@@ -258,110 +258,82 @@ export function clearActiveDutyMarker() {
|
||||
.forEach((m) => m.classList.remove("calendar-marker-active"));
|
||||
}
|
||||
|
||||
/** Timeout for hiding duty marker hint on mouseleave (delegated). */
|
||||
let dutyMarkerHideTimeout = null;
|
||||
|
||||
const DUTY_MARKER_SELECTOR = ".duty-marker, .unavailable-marker, .vacation-marker";
|
||||
|
||||
/**
|
||||
* Bind click tooltips for .info-btn (calendar event summaries).
|
||||
* Get or create the calendar event (info button) hint element.
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export function bindInfoButtonTooltips() {
|
||||
let hintEl = document.getElementById("calendarEventHint");
|
||||
if (!hintEl) {
|
||||
hintEl = document.createElement("div");
|
||||
hintEl.id = "calendarEventHint";
|
||||
hintEl.className = "calendar-event-hint";
|
||||
hintEl.setAttribute("role", "tooltip");
|
||||
hintEl.hidden = true;
|
||||
document.body.appendChild(hintEl);
|
||||
}
|
||||
if (!calendarEl) return;
|
||||
const lang = state.lang;
|
||||
calendarEl.querySelectorAll(".info-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const summary = btn.getAttribute("data-summary") || "";
|
||||
const content = t(lang, "hint.events") + "\n" + summary;
|
||||
if (hintEl.hidden || hintEl.textContent !== content) {
|
||||
hintEl.textContent = content;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
positionHint(hintEl, rect);
|
||||
hintEl.dataset.active = "1";
|
||||
} else {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!document._calendarHintBound) {
|
||||
document._calendarHintBound = true;
|
||||
document.addEventListener("click", () => {
|
||||
if (hintEl.dataset.active) {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && hintEl.dataset.active) {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
function getOrCreateCalendarEventHint() {
|
||||
let el = document.getElementById("calendarEventHint");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "calendarEventHint";
|
||||
el.className = "calendar-event-hint";
|
||||
el.setAttribute("role", "tooltip");
|
||||
el.hidden = true;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind hover/click tooltips for duty/unavailable/vacation markers.
|
||||
* Get or create the duty marker hint element.
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export function bindDutyMarkerTooltips() {
|
||||
let hintEl = document.getElementById("dutyMarkerHint");
|
||||
if (!hintEl) {
|
||||
hintEl = document.createElement("div");
|
||||
hintEl.id = "dutyMarkerHint";
|
||||
hintEl.className = "calendar-event-hint";
|
||||
hintEl.setAttribute("role", "tooltip");
|
||||
hintEl.hidden = true;
|
||||
document.body.appendChild(hintEl);
|
||||
function getOrCreateDutyMarkerHint() {
|
||||
let el = document.getElementById("dutyMarkerHint");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "dutyMarkerHint";
|
||||
el.className = "calendar-event-hint";
|
||||
el.setAttribute("role", "tooltip");
|
||||
el.hidden = true;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event delegation on calendarEl for info button and duty marker tooltips.
|
||||
* Call once at startup (e.g. alongside initDayDetail). No need to re-bind after render.
|
||||
*/
|
||||
export function initHints() {
|
||||
const calendarEventHint = getOrCreateCalendarEventHint();
|
||||
const dutyMarkerHint = getOrCreateDutyMarkerHint();
|
||||
if (!calendarEl) return;
|
||||
let hideTimeout = null;
|
||||
const selector = ".duty-marker, .unavailable-marker, .vacation-marker";
|
||||
calendarEl.querySelectorAll(selector).forEach((marker) => {
|
||||
marker.addEventListener("mouseenter", () => {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = null;
|
||||
}
|
||||
const html = getDutyMarkerHintHtml(marker);
|
||||
if (html) {
|
||||
hintEl.innerHTML = html;
|
||||
|
||||
calendarEl.addEventListener("click", (e) => {
|
||||
const btn = e.target instanceof HTMLElement ? e.target.closest(".info-btn") : null;
|
||||
if (btn) {
|
||||
e.stopPropagation();
|
||||
const summary = btn.getAttribute("data-summary") || "";
|
||||
const content = t(state.lang, "hint.events") + "\n" + summary;
|
||||
if (calendarEventHint.hidden || calendarEventHint.textContent !== content) {
|
||||
calendarEventHint.textContent = content;
|
||||
positionHint(calendarEventHint, btn.getBoundingClientRect());
|
||||
calendarEventHint.dataset.active = "1";
|
||||
} else {
|
||||
hintEl.textContent = getDutyMarkerHintContent(marker);
|
||||
calendarEventHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
calendarEventHint.hidden = true;
|
||||
calendarEventHint.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
const rect = marker.getBoundingClientRect();
|
||||
positionHint(hintEl, rect);
|
||||
hintEl.hidden = false;
|
||||
});
|
||||
marker.addEventListener("mouseleave", () => {
|
||||
if (hintEl.dataset.active) return;
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
hideTimeout = setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hideTimeout = null;
|
||||
}, 150);
|
||||
});
|
||||
marker.addEventListener("click", (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = e.target instanceof HTMLElement ? e.target.closest(DUTY_MARKER_SELECTOR) : null;
|
||||
if (marker) {
|
||||
e.stopPropagation();
|
||||
if (marker.classList.contains("calendar-marker-active")) {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
dutyMarkerHint.hidden = true;
|
||||
dutyMarkerHint.removeAttribute("data-active");
|
||||
}, 150);
|
||||
marker.classList.remove("calendar-marker-active");
|
||||
return;
|
||||
@@ -369,35 +341,89 @@ export function bindDutyMarkerTooltips() {
|
||||
clearActiveDutyMarker();
|
||||
const html = getDutyMarkerHintHtml(marker);
|
||||
if (html) {
|
||||
hintEl.innerHTML = html;
|
||||
dutyMarkerHint.innerHTML = html;
|
||||
} else {
|
||||
hintEl.textContent = getDutyMarkerHintContent(marker);
|
||||
dutyMarkerHint.textContent = getDutyMarkerHintContent(marker);
|
||||
}
|
||||
const rect = marker.getBoundingClientRect();
|
||||
positionHint(hintEl, rect);
|
||||
hintEl.hidden = false;
|
||||
hintEl.dataset.active = "1";
|
||||
positionHint(dutyMarkerHint, marker.getBoundingClientRect());
|
||||
dutyMarkerHint.hidden = false;
|
||||
dutyMarkerHint.dataset.active = "1";
|
||||
marker.classList.add("calendar-marker-active");
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!document._dutyMarkerHintBound) {
|
||||
document._dutyMarkerHintBound = true;
|
||||
|
||||
calendarEl.addEventListener("mouseover", (e) => {
|
||||
const marker = e.target instanceof HTMLElement ? e.target.closest(DUTY_MARKER_SELECTOR) : null;
|
||||
if (!marker) return;
|
||||
const related = e.relatedTarget instanceof Node ? e.relatedTarget : null;
|
||||
if (related && marker.contains(related)) return;
|
||||
if (dutyMarkerHideTimeout) {
|
||||
clearTimeout(dutyMarkerHideTimeout);
|
||||
dutyMarkerHideTimeout = null;
|
||||
}
|
||||
const html = getDutyMarkerHintHtml(marker);
|
||||
if (html) {
|
||||
dutyMarkerHint.innerHTML = html;
|
||||
} else {
|
||||
dutyMarkerHint.textContent = getDutyMarkerHintContent(marker);
|
||||
}
|
||||
positionHint(dutyMarkerHint, marker.getBoundingClientRect());
|
||||
dutyMarkerHint.hidden = false;
|
||||
});
|
||||
|
||||
calendarEl.addEventListener("mouseout", (e) => {
|
||||
const fromMarker = e.target instanceof HTMLElement ? e.target.closest(DUTY_MARKER_SELECTOR) : null;
|
||||
if (!fromMarker) return;
|
||||
const toMarker = e.relatedTarget instanceof HTMLElement ? e.relatedTarget.closest(DUTY_MARKER_SELECTOR) : null;
|
||||
if (toMarker) return;
|
||||
if (dutyMarkerHint.dataset.active) return;
|
||||
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
|
||||
dutyMarkerHideTimeout = setTimeout(() => {
|
||||
dutyMarkerHint.hidden = true;
|
||||
dutyMarkerHideTimeout = null;
|
||||
}, 150);
|
||||
});
|
||||
|
||||
if (!state.calendarHintBound) {
|
||||
state.calendarHintBound = true;
|
||||
document.addEventListener("click", () => {
|
||||
if (hintEl.dataset.active) {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
if (calendarEventHint.dataset.active) {
|
||||
calendarEventHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
calendarEventHint.hidden = true;
|
||||
calendarEventHint.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && calendarEventHint.dataset.active) {
|
||||
calendarEventHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
calendarEventHint.hidden = true;
|
||||
calendarEventHint.removeAttribute("data-active");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!state.dutyMarkerHintBound) {
|
||||
state.dutyMarkerHintBound = true;
|
||||
document.addEventListener("click", () => {
|
||||
if (dutyMarkerHint.dataset.active) {
|
||||
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
dutyMarkerHint.hidden = true;
|
||||
dutyMarkerHint.removeAttribute("data-active");
|
||||
clearActiveDutyMarker();
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && hintEl.dataset.active) {
|
||||
hintEl.classList.remove("calendar-event-hint--visible");
|
||||
if (e.key === "Escape" && dutyMarkerHint.dataset.active) {
|
||||
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
|
||||
setTimeout(() => {
|
||||
hintEl.hidden = true;
|
||||
hintEl.removeAttribute("data-active");
|
||||
dutyMarkerHint.hidden = true;
|
||||
dutyMarkerHint.removeAttribute("data-active");
|
||||
clearActiveDutyMarker();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user