WIP: February 2026 event improvements and calendar refactor #1
@ -643,6 +643,92 @@ function initNaturalEventParser(root = document) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const weekdayCodeMap = {
|
||||
su: 'SU', sun: 'SU', sunday: 'SU', sundays: 'SU',
|
||||
mo: 'MO', mon: 'MO', monday: 'MO', mondays: 'MO',
|
||||
tu: 'TU', tue: 'TU', tues: 'TU', tuesday: 'TU', tuesdays: 'TU',
|
||||
we: 'WE', wed: 'WE', wednesday: 'WE', wednesdays: 'WE',
|
||||
th: 'TH', thu: 'TH', thur: 'TH', thurs: 'TH', thursday: 'TH', thursdays: 'TH',
|
||||
fr: 'FR', fri: 'FR', friday: 'FR', fridays: 'FR',
|
||||
sa: 'SA', sat: 'SA', saturday: 'SA', saturdays: 'SA',
|
||||
};
|
||||
|
||||
const weekdayNameMap = {
|
||||
SU: 'Sunday',
|
||||
MO: 'Monday',
|
||||
TU: 'Tuesday',
|
||||
WE: 'Wednesday',
|
||||
TH: 'Thursday',
|
||||
FR: 'Friday',
|
||||
SA: 'Saturday',
|
||||
};
|
||||
|
||||
const weekdayCodeFromDate = (date) => ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][date.getDay()];
|
||||
|
||||
const parseRecurrenceToken = (text) => {
|
||||
const repeatMatch = text.match(/\brepeats?\b([\s\S]*)$/i);
|
||||
if (!repeatMatch) return null;
|
||||
|
||||
const tail = (repeatMatch[1] || '').trim().toLowerCase();
|
||||
const recurrence = {
|
||||
frequency: null,
|
||||
interval: 1,
|
||||
weekdays: [],
|
||||
index: repeatMatch.index ?? 0,
|
||||
};
|
||||
|
||||
const everyNumbered = tail.match(/\bevery\s+(\d+)\s*(day|week|month|year)s?\b/i);
|
||||
if (everyNumbered) {
|
||||
const count = parseInt(everyNumbered[1], 10);
|
||||
const unit = everyNumbered[2].toLowerCase();
|
||||
if (!Number.isNaN(count) && count > 0) {
|
||||
recurrence.interval = count;
|
||||
}
|
||||
recurrence.frequency = {
|
||||
day: 'daily',
|
||||
week: 'weekly',
|
||||
month: 'monthly',
|
||||
year: 'yearly',
|
||||
}[unit] || null;
|
||||
} else if (/\b(daily|every day)\b/i.test(tail)) {
|
||||
recurrence.frequency = 'daily';
|
||||
} else if (/\b(weekly|every week)\b/i.test(tail)) {
|
||||
recurrence.frequency = 'weekly';
|
||||
} else if (/\b(monthly|every month)\b/i.test(tail)) {
|
||||
recurrence.frequency = 'monthly';
|
||||
} else if (/\b(yearly|every year)\b/i.test(tail)) {
|
||||
recurrence.frequency = 'yearly';
|
||||
}
|
||||
|
||||
if (!recurrence.frequency) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recurrence.frequency === 'weekly') {
|
||||
const onMatch = tail.match(/\bon\s+(.+)$/i);
|
||||
if (onMatch) {
|
||||
const weekdays = [];
|
||||
const tokens = onMatch[1]
|
||||
.replace(/[,/]/g, ' ')
|
||||
.replace(/\band\b/g, ' ')
|
||||
.split(/\s+/)
|
||||
.map((token) => token.trim().toLowerCase().replace(/[^a-z]/g, ''))
|
||||
.filter(Boolean);
|
||||
|
||||
tokens.forEach((token) => {
|
||||
const code = weekdayCodeMap[token];
|
||||
if (code && !weekdays.includes(code)) {
|
||||
weekdays.push(code);
|
||||
}
|
||||
});
|
||||
|
||||
recurrence.weekdays = weekdays;
|
||||
}
|
||||
}
|
||||
|
||||
return recurrence;
|
||||
};
|
||||
|
||||
const parseLocationToken = (text) => {
|
||||
const lower = text.toLowerCase();
|
||||
const index = lower.lastIndexOf(' at ');
|
||||
@ -656,11 +742,12 @@ function initNaturalEventParser(root = document) {
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const parseTitleToken = (text, dateToken, timeToken, relativeToken, locationToken) => {
|
||||
const parseTitleToken = (text, dateToken, timeToken, relativeToken, recurrenceToken, locationToken) => {
|
||||
const boundaries = [];
|
||||
if (dateToken) boundaries.push(dateToken.index);
|
||||
if (timeToken) boundaries.push(timeToken.index);
|
||||
if (relativeToken) boundaries.push(relativeToken.index);
|
||||
if (recurrenceToken) boundaries.push(recurrenceToken.index);
|
||||
|
||||
const lower = text.toLowerCase();
|
||||
const durationIndex = lower.indexOf(' for ');
|
||||
@ -684,11 +771,12 @@ function initNaturalEventParser(root = document) {
|
||||
if (!trimmed) return null;
|
||||
|
||||
const relativeToken = parseRelativeToken(trimmed, baseDate);
|
||||
const recurrenceToken = parseRecurrenceToken(trimmed);
|
||||
const dateToken = parseDateToken(trimmed, baseDate);
|
||||
const timeToken = parseTimeToken(trimmed);
|
||||
const durationMinutes = parseDurationMinutes(trimmed);
|
||||
const location = parseLocationToken(trimmed);
|
||||
const title = parseTitleToken(trimmed, dateToken, timeToken, relativeToken, location);
|
||||
const title = parseTitleToken(trimmed, dateToken, timeToken, relativeToken, recurrenceToken, location);
|
||||
const allDay = /\ball(?:\s|-)?day\b/i.test(trimmed);
|
||||
|
||||
return {
|
||||
@ -697,6 +785,7 @@ function initNaturalEventParser(root = document) {
|
||||
dateToken,
|
||||
timeToken,
|
||||
relativeToken,
|
||||
recurrenceToken,
|
||||
durationMinutes,
|
||||
allDay,
|
||||
};
|
||||
@ -725,6 +814,108 @@ function initNaturalEventParser(root = document) {
|
||||
renderModalAside(dialog, [], true);
|
||||
};
|
||||
|
||||
const clearRecurrenceInputs = (form) => {
|
||||
const frequencyInput = form.querySelector('select[name="repeat_frequency"]');
|
||||
const intervalInput = form.querySelector('input[name="repeat_interval"]');
|
||||
const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]'));
|
||||
const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]'));
|
||||
const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]'));
|
||||
|
||||
if (frequencyInput) {
|
||||
frequencyInput.value = '';
|
||||
frequencyInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (intervalInput) {
|
||||
intervalInput.value = '1';
|
||||
}
|
||||
weekdayInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
monthlyModeInputs.forEach((input) => {
|
||||
input.checked = input.value === 'days';
|
||||
});
|
||||
monthDayInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
};
|
||||
|
||||
const applyRecurrenceInputs = (form, recurrenceToken, anchorDate) => {
|
||||
const frequencyInput = form.querySelector('select[name="repeat_frequency"]');
|
||||
const intervalInput = form.querySelector('input[name="repeat_interval"]');
|
||||
const weekdayInputs = Array.from(form.querySelectorAll('input[name="repeat_weekdays[]"]'));
|
||||
const monthlyModeInputs = Array.from(form.querySelectorAll('input[name="repeat_monthly_mode"]'));
|
||||
const monthDayInputs = Array.from(form.querySelectorAll('input[name="repeat_month_days[]"]'));
|
||||
|
||||
if (!frequencyInput || !recurrenceToken?.frequency) {
|
||||
return;
|
||||
}
|
||||
|
||||
frequencyInput.value = recurrenceToken.frequency;
|
||||
frequencyInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
if (intervalInput) {
|
||||
intervalInput.value = String(recurrenceToken.interval || 1);
|
||||
}
|
||||
|
||||
if (recurrenceToken.frequency === 'weekly') {
|
||||
let weekdays = recurrenceToken.weekdays || [];
|
||||
if (!weekdays.length && anchorDate) {
|
||||
weekdays = [weekdayCodeFromDate(anchorDate)];
|
||||
}
|
||||
weekdayInputs.forEach((input) => {
|
||||
input.checked = weekdays.includes(input.value);
|
||||
});
|
||||
} else {
|
||||
weekdayInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (recurrenceToken.frequency === 'monthly') {
|
||||
monthlyModeInputs.forEach((input) => {
|
||||
input.checked = input.value === 'days';
|
||||
});
|
||||
|
||||
const dayOfMonth = anchorDate ? anchorDate.getDate() : null;
|
||||
monthDayInputs.forEach((input) => {
|
||||
input.checked = dayOfMonth ? Number(input.value) === dayOfMonth : false;
|
||||
});
|
||||
} else {
|
||||
monthDayInputs.forEach((input) => {
|
||||
input.checked = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const recurrenceSummary = (recurrenceToken, anchorDate) => {
|
||||
if (!recurrenceToken?.frequency) return '';
|
||||
|
||||
const interval = recurrenceToken.interval || 1;
|
||||
|
||||
if (recurrenceToken.frequency === 'daily') {
|
||||
return interval === 1 ? 'Daily' : `Every ${interval} days`;
|
||||
}
|
||||
|
||||
if (recurrenceToken.frequency === 'weekly') {
|
||||
const days = (recurrenceToken.weekdays || []).length
|
||||
? recurrenceToken.weekdays
|
||||
: (anchorDate ? [weekdayCodeFromDate(anchorDate)] : []);
|
||||
const dayLabels = days.map((code) => weekdayNameMap[code] || code);
|
||||
const head = interval === 1 ? 'Every week' : `Every ${interval} weeks`;
|
||||
return dayLabels.length ? `${head} on ${dayLabels.join(' and ')}` : head;
|
||||
}
|
||||
|
||||
if (recurrenceToken.frequency === 'monthly') {
|
||||
return interval === 1 ? 'Monthly' : `Every ${interval} months`;
|
||||
}
|
||||
|
||||
if (recurrenceToken.frequency === 'yearly') {
|
||||
return interval === 1 ? 'Yearly' : `Every ${interval} years`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const applyParsedData = (input) => {
|
||||
const dialog = input.closest('dialog');
|
||||
const modal = dialog?.querySelector(SELECTORS.modalContent);
|
||||
@ -793,6 +984,14 @@ function initNaturalEventParser(root = document) {
|
||||
endInput.value = toLocalDatetimeInputValue(end);
|
||||
}
|
||||
|
||||
if (parsed.recurrenceToken) {
|
||||
applyRecurrenceInputs(form, parsed.recurrenceToken, baseDate);
|
||||
input.dataset.nlRecurrenceApplied = '1';
|
||||
} else if (input.dataset.nlRecurrenceApplied === '1') {
|
||||
clearRecurrenceInputs(form);
|
||||
input.dataset.nlRecurrenceApplied = '0';
|
||||
}
|
||||
|
||||
const summaryItems = [];
|
||||
if (parsed.title) {
|
||||
summaryItems.push({ label: 'Title', value: parsed.title });
|
||||
@ -814,6 +1013,12 @@ function initNaturalEventParser(root = document) {
|
||||
summaryItems.push({ label: 'Date', value: summaryDate });
|
||||
}
|
||||
}
|
||||
if (parsed.recurrenceToken) {
|
||||
const repeat = recurrenceSummary(parsed.recurrenceToken, baseDate);
|
||||
if (repeat) {
|
||||
summaryItems.push({ label: 'Repeat', value: repeat });
|
||||
}
|
||||
}
|
||||
|
||||
if (modal?.classList.contains('natural-collapsed')) {
|
||||
renderModalAside(dialog, summaryItems, true);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user