mirror of
https://github.com/Xevion/glance.git
synced 2025-12-06 01:15:08 -06:00
213 lines
7.3 KiB
JavaScript
213 lines
7.3 KiB
JavaScript
import { directions, easeOutQuint, slideFade } from "./animations.js";
|
|
import { elem, repeat, text } from "./templating.js";
|
|
|
|
const FULL_MONTH_SLOTS = 7*6;
|
|
const WEEKDAY_ABBRS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
|
|
const leftArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
|
</svg>`;
|
|
|
|
const rightArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
</svg>`;
|
|
|
|
const undoArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
|
|
</svg>`;
|
|
|
|
const [datesExitLeft, datesExitRight] = directions(
|
|
slideFade, { distance: "2rem", duration: 120, offset: 1 },
|
|
"left", "right"
|
|
);
|
|
|
|
const [datesEntranceLeft, datesEntranceRight] = directions(
|
|
slideFade, { distance: "0.8rem", duration: 500, easing: easeOutQuint },
|
|
"left", "right"
|
|
);
|
|
|
|
const undoEntrance = slideFade({ direction: "left", distance: "100%", duration: 300 });
|
|
|
|
export default function(element) {
|
|
element.swap(Calendar(
|
|
Number(element.dataset.firstDayOfWeek ?? 1)
|
|
));
|
|
}
|
|
|
|
// TODO: when viewing the previous/next month, display the current date if it's within the spill-over days
|
|
function Calendar(firstDay) {
|
|
let header, dates;
|
|
let advanceTimeTicker;
|
|
let now = new Date();
|
|
let activeDate;
|
|
|
|
const update = (newDate) => {
|
|
header.component.update(now, newDate);
|
|
dates.component.update(now, newDate);
|
|
activeDate = newDate;
|
|
};
|
|
|
|
const autoAdvanceNow = () => {
|
|
advanceTimeTicker = setTimeout(() => {
|
|
// TODO: don't auto advance if looking at a different month
|
|
update(now = new Date());
|
|
autoAdvanceNow();
|
|
}, msTillNextDay());
|
|
};
|
|
|
|
const adjacentMonth = (dir) => new Date(activeDate.getFullYear(), activeDate.getMonth() + dir, 1);
|
|
const nextClicked = () => update(adjacentMonth(1));
|
|
const prevClicked = () => update(adjacentMonth(-1));
|
|
const undoClicked = () => update(now);
|
|
|
|
const calendar = elem().classes("calendar").append(
|
|
header = Header(nextClicked, prevClicked, undoClicked),
|
|
dates = Dates(firstDay)
|
|
);
|
|
|
|
update(now);
|
|
autoAdvanceNow();
|
|
|
|
return calendar.component({
|
|
suspend: () => clearTimeout(advanceTimeTicker)
|
|
});
|
|
}
|
|
|
|
function Header(nextClicked, prevClicked, undoClicked) {
|
|
let month, monthNumber, year, undo;
|
|
const button = () => elem("button").classes("calendar-header-button");
|
|
|
|
const monthAndYear = elem().classes("size-h2", "color-highlight").append(
|
|
month = text(),
|
|
" ",
|
|
year = elem("span").classes("size-h3"),
|
|
undo = button()
|
|
.hide()
|
|
.classes("calendar-undo-button")
|
|
.attr("title", "Back to current month")
|
|
.on("click", undoClicked)
|
|
.html(undoArrowSvg)
|
|
);
|
|
|
|
const monthSwitcher = elem()
|
|
.classes("flex", "gap-7", "items-center")
|
|
.append(
|
|
button()
|
|
.attr("title", "Previous month")
|
|
.on("click", prevClicked)
|
|
.html(leftArrowSvg),
|
|
monthNumber = elem()
|
|
.classes("color-highlight")
|
|
.styles({ marginTop: "0.1rem" }),
|
|
button()
|
|
.attr("title", "Next month")
|
|
.on("click", nextClicked)
|
|
.html(rightArrowSvg),
|
|
);
|
|
|
|
return elem().classes("flex", "justify-between", "items-center").append(
|
|
monthAndYear,
|
|
monthSwitcher
|
|
).component({
|
|
update: function (now, newDate) {
|
|
month.text(MONTH_NAMES[newDate.getMonth()]);
|
|
year.text(newDate.getFullYear());
|
|
const m = newDate.getMonth() + 1;
|
|
monthNumber.text((m < 10 ? "0" : "") + m);
|
|
|
|
if (!datesWithinSameMonth(now, newDate)) {
|
|
if (undo.isHidden()) undo.show().animate(undoEntrance);
|
|
} else {
|
|
undo.hide();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
}
|
|
|
|
function Dates(firstDay) {
|
|
let dates, lastRenderedDate;
|
|
|
|
const updateFullMonth = function(now, newDate) {
|
|
const firstWeekday = new Date(newDate.getFullYear(), newDate.getMonth(), 1).getDay();
|
|
const previousMonthSpilloverDays = (firstWeekday - firstDay + 7) % 7 || 7;
|
|
const currentMonthDays = daysInMonth(newDate.getFullYear(), newDate.getMonth());
|
|
const nextMonthSpilloverDays = FULL_MONTH_SLOTS - (previousMonthSpilloverDays + currentMonthDays);
|
|
const previousMonthDays = daysInMonth(newDate.getFullYear(), newDate.getMonth() - 1)
|
|
const isCurrentMonth = datesWithinSameMonth(now, newDate);
|
|
const currentDate = now.getDate();
|
|
|
|
let children = dates.children;
|
|
let index = 0;
|
|
|
|
for (let i = 0; i < FULL_MONTH_SLOTS; i++) {
|
|
children[i].clearClasses("calendar-spillover-date", "calendar-current-date");
|
|
}
|
|
|
|
for (let i = 0; i < previousMonthSpilloverDays; i++, index++) {
|
|
children[index].classes("calendar-spillover-date").text(
|
|
previousMonthDays - previousMonthSpilloverDays + i + 1
|
|
)
|
|
}
|
|
|
|
for (let i = 1; i <= currentMonthDays; i++, index++) {
|
|
children[index]
|
|
.classesIf(isCurrentMonth && i === currentDate, "calendar-current-date")
|
|
.text(i);
|
|
}
|
|
|
|
for (let i = 0; i < nextMonthSpilloverDays; i++, index++) {
|
|
children[index].classes("calendar-spillover-date").text(i + 1);
|
|
}
|
|
|
|
lastRenderedDate = newDate;
|
|
};
|
|
|
|
const update = function(now, newDate) {
|
|
if (lastRenderedDate === undefined || datesWithinSameMonth(newDate, lastRenderedDate)) {
|
|
updateFullMonth(now, newDate);
|
|
return;
|
|
}
|
|
|
|
const next = newDate > lastRenderedDate;
|
|
dates.animateUpdate(
|
|
() => updateFullMonth(now, newDate),
|
|
next ? datesExitLeft : datesExitRight,
|
|
next ? datesEntranceRight : datesEntranceLeft,
|
|
);
|
|
}
|
|
|
|
return elem().append(
|
|
elem().classes("calendar-dates", "margin-top-15").append(
|
|
...repeat(7, (i) => elem().classes("size-h6", "color-subdue").text(
|
|
WEEKDAY_ABBRS[(firstDay + i) % 7]
|
|
))
|
|
),
|
|
|
|
dates = elem().classes("calendar-dates", "margin-top-3").append(
|
|
...elem().classes("calendar-date").duplicate(FULL_MONTH_SLOTS)
|
|
)
|
|
).component({ update });
|
|
}
|
|
|
|
function datesWithinSameMonth(d1, d2) {
|
|
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
|
|
}
|
|
|
|
function daysInMonth(year, month) {
|
|
return new Date(year, month + 1, 0).getDate();
|
|
}
|
|
|
|
function msTillNextDay(now) {
|
|
now = now || new Date();
|
|
|
|
return 86_400_000 - (
|
|
now.getMilliseconds() +
|
|
now.getSeconds() * 1000 +
|
|
now.getMinutes() * 60_000 +
|
|
now.getHours() * 3_600_000
|
|
);
|
|
}
|