Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s

This commit is contained in:
Shinuwa 2026-08-20 15:48:24 +02:00
parent 4f73e0068b
commit 992665f4ea
39 changed files with 4331 additions and 36 deletions

View file

@ -0,0 +1,123 @@
// Rôle : normalise et compacte les événements du calendrier hebdomadaire et mensuel.
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, CALENDAR_VIEW_MODES, formatLocalDateKey, getLocalDayIndex, parseLocalDateKey, WEEK_DAY_COUNT } from "../../modules/calendarUtils.js";
import { uid } from "./shared.js";
const VIEW_MODES = new Set(CALENDAR_VIEW_MODES);
const ALERT_MODES = new Set(CALENDAR_ALERT_MODES);
const ALERT_OFFSETS = new Set(CALENDAR_ALERT_OFFSETS);
const COLORS = new Set(CALENDAR_COLORS);
const MONTHLY_REPEAT_BY = new Set(CALENDAR_MONTHLY_REPEAT_BY);
const MONTH_REPEAT_MODES = new Set(CALENDAR_MONTH_REPEAT_MODES);
function normalizeDayIndex(value) {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 && parsed < WEEK_DAY_COUNT ? parsed : -1;
}
function normalizeMinuteOfDay(value) {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 && parsed < 24 * 60 ? parsed : -1;
}
function normalizeDateKey(value) {
const date = parseLocalDateKey(value);
return date ? formatLocalDateKey(date) : "";
}
function normalizeExcludedOccurrences(value) {
if (!Array.isArray(value)) return [];
return [...new Set(value.map(normalizeDateKey).filter(Boolean))].sort();
}
function normalizeAlertOffsets(value) {
const rawOffsets = Array.isArray(value) ? value : [value];
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => ALERT_OFFSETS.has(offset)))];
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
}
export function normalizeCalendarData(data) {
const viewMode = VIEW_MODES.has(data?.viewMode) ? data.viewMode : "week";
const events = (Array.isArray(data?.events) ? data.events : [])
.map((event) => {
const title = String(event?.title || "").trim();
const dayIndex = normalizeDayIndex(event?.dayIndex);
const startDate = normalizeDateKey(event?.startDate);
const endDate = normalizeDateKey(event?.endDate || event?.startDate);
const parsedStartMinutes = normalizeMinuteOfDay(event?.startMinutes);
const startMinutes = viewMode === "month" && parsedStartMinutes < 0 ? 0 : parsedStartMinutes;
const monthlyRepeatBy = MONTHLY_REPEAT_BY.has(event?.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
const monthRepeat = MONTH_REPEAT_MODES.has(event?.repeat) ? event.repeat : event?.repeatable === false ? "none" : "monthly";
const hasValidDateTarget = viewMode === "month"
? startDate && endDate && endDate >= startDate
: dayIndex >= 0;
if (!title || !hasValidDateTarget || startMinutes < 0) return null;
const endMinutes = normalizeMinuteOfDay(event?.endMinutes);
const alertMode = ALERT_MODES.has(event?.alertMode) ? event.alertMode : "off";
const alertOffsetMinutes = normalizeAlertOffsets(event?.alertOffsetMinutes);
const color = COLORS.has(event?.color) ? event.color : "violet";
const repeatable = viewMode === "month" ? monthRepeat !== "none" : event?.repeatable !== false;
const normalized = {
id: event?.id || uid("calendar"),
title,
startMinutes,
repeat: viewMode === "month" ? monthlyRepeatBy === "monthDay" && monthRepeat === "weekly" ? "none" : monthRepeat : repeatable ? "weekly" : "none",
repeatable,
alertMode,
alertOffsetMinutes,
color
};
if (viewMode === "month") {
normalized.monthlyRepeatBy = monthlyRepeatBy;
normalized.startDate = startDate;
normalized.endDate = endDate;
normalized.dayIndex = dayIndex >= 0 ? dayIndex : getLocalDayIndex(parseLocalDateKey(startDate));
normalized.excludedOccurrences = normalizeExcludedOccurrences(event?.excludedOccurrences);
} else {
normalized.dayIndex = dayIndex;
}
if (endMinutes > startMinutes) normalized.endMinutes = endMinutes;
return normalized;
})
.filter(Boolean);
return {
initialized: data?.initialized === true || events.length > 0,
viewMode,
weekStartsOn: Number(data?.weekStartsOn) === 0 ? 0 : 1,
events
};
}
export function compactCalendarDataForStorage(value) {
const normalized = normalizeCalendarData(value);
const compact = {};
if (normalized.initialized) compact.initialized = true;
if (normalized.viewMode !== "week") compact.viewMode = normalized.viewMode;
if (normalized.weekStartsOn !== 1) compact.weekStartsOn = normalized.weekStartsOn;
if (normalized.events.length) {
compact.events = normalized.events.map((event) => {
const compactEvent = {
id: event.id,
title: event.title
};
if (normalized.viewMode !== "month" || event.startMinutes !== 0) compactEvent.startMinutes = event.startMinutes;
if (normalized.viewMode === "month") {
compactEvent.startDate = event.startDate;
if (event.endDate !== event.startDate) compactEvent.endDate = event.endDate;
if (event.repeat !== "monthly") compactEvent.repeat = event.repeat;
if (event.monthlyRepeatBy !== "monthDay") compactEvent.monthlyRepeatBy = event.monthlyRepeatBy;
if (event.excludedOccurrences.length) compactEvent.excludedOccurrences = event.excludedOccurrences;
} else {
compactEvent.dayIndex = event.dayIndex;
}
if (normalized.viewMode !== "month" && event.endMinutes) compactEvent.endMinutes = event.endMinutes;
if (normalized.viewMode !== "month" && event.repeatable === false) compactEvent.repeatable = false;
if (event.alertMode !== "off") compactEvent.alertMode = event.alertMode;
if (event.alertMode !== "off" && (event.alertOffsetMinutes.length !== 1 || event.alertOffsetMinutes[0] !== 10)) compactEvent.alertOffsetMinutes = event.alertOffsetMinutes;
if (event.color !== "violet") compactEvent.color = event.color;
return compactEvent;
});
}
return Object.keys(compact).length ? compact : null;
}

View file

@ -1,5 +1,6 @@
// Rôle : expose le registre strict des compacteurs par type d'outil toolbox.
import { compactCalculatorDataForStorage } from "./calculator.js";
import { compactCalendarDataForStorage } from "./calendar.js";
import { compactChecklistDataForStorage } from "./checklist.js";
import { compactCombosDataForStorage } from "./combos.js";
import { compactCountersDataForStorage } from "./counters.js";
@ -20,6 +21,7 @@ export const MODULE_DATA_COMPACTORS = {
counters: compactCountersDataForStorage,
combos: compactCombosDataForStorage,
calculator: compactCalculatorDataForStorage,
calendar: compactCalendarDataForStorage,
table: compactTableDataForStorage,
timer: compactTimerDataForStorage,
taskPlanner: compactTaskPlannerDataForStorage,

View file

@ -15,13 +15,23 @@ const NOTEPAD_RGB_COLOR_MAP = {
};
function escapeHtml(value) {
return String(value || "")
return decodeBasicEntitiesDeep(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function decodeBasicEntitiesDeep(value) {
let current = String(value || "");
for (let index = 0; index < 5; index += 1) {
const next = decodeBasicEntities(current);
if (next === current) return next;
current = next;
}
return current;
}
function decodeBasicEntities(value) {
return String(value || "")
.replace(/&nbsp;/gi, " ")

View file

@ -9,6 +9,7 @@ export const ID_PREFIXES = {
counter: "c",
combo: "o",
calc: "r",
calendar: "h",
marker: "k",
timer: "z",
task: "a",

View file

@ -4,6 +4,7 @@ import { MODULE_DATA_COMPACTORS } from "./modules/index.js";
import { ID_PREFIXES, uid } from "./modules/shared.js";
export { normalizeCalculatorData } from "./modules/calculator.js";
export { normalizeCalendarData } from "./modules/calendar.js";
export { clampQty, normalizeChecklistData } from "./modules/checklist.js";
export { normalizeCombosData } from "./modules/combos.js";
export { normalizeCountersData } from "./modules/counters.js";
@ -250,6 +251,12 @@ function remapModuleDataForExport(type, data, nextId) {
return remapped;
}
if (type === "calendar") {
const remapped = { ...compact };
if (compact.events) remapped.events = compact.events.map((event) => ({ ...event, id: nextId("calendar") }));
return remapped;
}
if (type === "taskPlanner") {
const taskIdMap = new Map();
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));