fa442b1fef
- Self-contained YML: all CSS inlined (no global.css edit needed) - Vanilla JS client with MutationObserver bootstrap - Slide+fade month navigation via rAF (works in this Dynacat build) - Popover portaled to <body> to escape backdrop-filter containing blocks - Per-service enabled flag, unconfigured services silently skipped - Status <ul> exposes per-service configured/ok flags for client hints
635 lines
23 KiB
JavaScript
635 lines
23 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
var MONTH_NAMES = [
|
|
'January', 'February', 'March', 'April', 'May', 'June',
|
|
'July', 'August', 'September', 'October', 'November', 'December'
|
|
];
|
|
var WEEKDAYS_MON_FIRST = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
|
|
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
|
|
|
|
function parseISODate(s) {
|
|
var parts = s.split('-');
|
|
return {
|
|
y: parseInt(parts[0], 10),
|
|
m: parseInt(parts[1], 10) - 1,
|
|
d: parseInt(parts[2], 10)
|
|
};
|
|
}
|
|
|
|
function formatISO(y, m, d) {
|
|
return y + '-' + pad2(m + 1) + '-' + pad2(d);
|
|
}
|
|
|
|
function daysInMonth(y, m) {
|
|
return new Date(y, m + 1, 0).getDate();
|
|
}
|
|
|
|
function collectReleases(widget) {
|
|
var byDate = {};
|
|
var lis = widget.querySelectorAll('.arr-release-data > li');
|
|
for (var i = 0; i < lis.length; i++) {
|
|
var li = lis[i];
|
|
var date = li.getAttribute('data-date');
|
|
if (!date) continue;
|
|
if (!byDate[date]) byDate[date] = [];
|
|
byDate[date].push({
|
|
service: li.getAttribute('data-service'),
|
|
date: date,
|
|
title: li.getAttribute('data-title') || '',
|
|
subtitle: li.getAttribute('data-subtitle') || '',
|
|
link: li.getAttribute('data-link') || '#',
|
|
cover: li.getAttribute('data-cover') || ''
|
|
});
|
|
}
|
|
return byDate;
|
|
}
|
|
|
|
// Build the rows for the current month. The calendar always renders
|
|
// exactly 6 weeks (42 day cells) so its height is constant across all
|
|
// months. The first row of cells starts on `firstDayOfWeek` (Monday)
|
|
// and may include spillover days from the previous month; the last
|
|
// row may include spillover days from the next month. This matches
|
|
// Glance's built-in calendar (FULL_MONTH_SLOTS = 7 * 6).
|
|
//
|
|
// Spillover formula matches Glance's calendar.js exactly: when the
|
|
// month starts on firstDayOfWeek itself, we treat that as "a full
|
|
// previous week" (7 spillover days), not zero — this keeps every row
|
|
// aligned to a complete Mon..Sun week.
|
|
function buildRows(year, month, firstDayOfWeek) {
|
|
if (firstDayOfWeek === undefined) firstDayOfWeek = 0; // 0 = Monday
|
|
var firstDay = new Date(year, month, 1);
|
|
// Monday-first: convert JS getDay() (0=Sun..6=Sat) to 0=Mon..6=Sun
|
|
var firstWeekday = (firstDay.getDay() + 6) % 7;
|
|
var totalDays = daysInMonth(year, month);
|
|
var prevMonthDays = daysInMonth(year, month - 1);
|
|
|
|
// Header row (weekday labels) — separate from the day grid below.
|
|
var rows = [];
|
|
var headerRow = [];
|
|
for (var w = 0; w < 7; w++) {
|
|
headerRow.push({ type: 'weekday', text: WEEKDAYS_MON_FIRST[w] });
|
|
}
|
|
rows.push(headerRow);
|
|
|
|
// Distribute 42 cells into: prev-month spillover, current month,
|
|
// next-month spillover. Matches Glance's `previousMonthSpilloverDays`
|
|
// formula: when firstWeekday == firstDayOfWeek we render a full week
|
|
// of prev-month spillover (7), not 0.
|
|
var prevSpill = (firstWeekday - firstDayOfWeek + 7) % 7 || 7; // 1..7
|
|
var currentDays = totalDays; // 28..31
|
|
var nextSpill = 42 - (prevSpill + currentDays); // 4..14
|
|
// Sanity guard — the math always adds to 42 for valid months, but if
|
|
// something pathological happens, clamp to keep the layout stable.
|
|
if (nextSpill < 0) nextSpill = 0;
|
|
|
|
// Build 6 rows of 7 cells each, filling in:
|
|
// 1..prevSpill → previous-month spillover (from prev month)
|
|
// prevSpill+1..prevSpill+currentDays → current month
|
|
// the rest → next-month spillover
|
|
var cellIndex = 0;
|
|
for (var r = 0; r < 6; r++) {
|
|
var row = [];
|
|
for (var d = 0; d < 7; d++) {
|
|
cellIndex++;
|
|
if (cellIndex <= prevSpill) {
|
|
// Previous-month spillover: day number is prevMonthDays - prevSpill + cellIndex
|
|
row.push({
|
|
type: 'day',
|
|
day: prevMonthDays - prevSpill + cellIndex,
|
|
outsideMonth: true,
|
|
iso: null,
|
|
releases: []
|
|
});
|
|
} else if (cellIndex <= prevSpill + currentDays) {
|
|
var dayOfMonth = cellIndex - prevSpill;
|
|
var iso = formatISO(year, month, dayOfMonth);
|
|
row.push({
|
|
type: 'day',
|
|
day: dayOfMonth,
|
|
outsideMonth: false,
|
|
iso: iso,
|
|
releases: [] // filled in by renderGrid
|
|
});
|
|
} else {
|
|
// Next-month spillover: day number starts at 1
|
|
var nextDay = cellIndex - (prevSpill + currentDays);
|
|
row.push({
|
|
type: 'day',
|
|
day: nextDay,
|
|
outsideMonth: true,
|
|
iso: null,
|
|
releases: []
|
|
});
|
|
}
|
|
}
|
|
rows.push(row);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
// Animation constants matching the built-in Glance calendar's slide+fade
|
|
// effect when navigating between months. Uses requestAnimationFrame to
|
|
// manually drive the animation. This is the most reliable approach
|
|
// because it doesn't depend on CSS transitions, Web Animations API, or
|
|
// any specific browser behavior.
|
|
var ANIM_DURATION_ENTER = 700;
|
|
// Slide distance is 0.8rem — converted to pixels at call time using
|
|
// the actual root font-size (Dynacat sets it to 10px, so 0.8rem ≈ 8px).
|
|
var ANIM_DISTANCE_REM = 0.8;
|
|
|
|
// Render the new content first (so navigation is responsive even if the
|
|
// animation fails for any reason), then apply the slide+fade animation
|
|
// as a visual flourish.
|
|
function animateGridChange(widgetOrGrid, renderNew, dir) {
|
|
if (dir !== -1) dir = 1;
|
|
|
|
// Resolve the live grid element.
|
|
var gridEl = widgetOrGrid;
|
|
if (gridEl && gridEl.classList && !gridEl.classList.contains('arr-calendar-grid')) {
|
|
gridEl = widgetOrGrid.querySelector('.arr-calendar-grid');
|
|
}
|
|
if (!gridEl) {
|
|
renderNew();
|
|
return;
|
|
}
|
|
|
|
// Compute the actual pixel distance based on the root font size.
|
|
// (CSS rem resolves to the root font-size; Dynacat sets it to 10px.)
|
|
var rootStyle = window.getComputedStyle(document.documentElement);
|
|
var rootFontSize = parseFloat(rootStyle.fontSize) || 10;
|
|
var distancePx = ANIM_DISTANCE_REM * rootFontSize;
|
|
|
|
// Direction:
|
|
// NEXT (forward in time): new month slides in from the RIGHT
|
|
// PREV (backward in time): new month slides in from the LEFT
|
|
var startX = dir > 0 ? distancePx : -distancePx;
|
|
|
|
// Render the new content first.
|
|
renderNew();
|
|
|
|
// Animate via rAF. We use a "linear-tween" approach: compute progress
|
|
// over the duration, interpolate opacity and transform.
|
|
var startTime = null;
|
|
function step(now) {
|
|
if (startTime === null) startTime = now;
|
|
var elapsed = now - startTime;
|
|
var progress = Math.min(elapsed / ANIM_DURATION_ENTER, 1);
|
|
// Use the same easing as Glance's calendar: cubic-bezier(0.22, 1, 0.36, 1)
|
|
// Approximated as a simple easeOutCubic for performance.
|
|
var eased = 1 - Math.pow(1 - progress, 3);
|
|
gridEl.style.opacity = String(eased);
|
|
gridEl.style.transform = 'translateX(' + (startX * (1 - eased)) + 'px)';
|
|
if (progress < 1) {
|
|
requestAnimationFrame(step);
|
|
} else {
|
|
// Clean up inline styles when done
|
|
gridEl.style.opacity = '';
|
|
gridEl.style.transform = '';
|
|
}
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
// Animate the undo button sliding in from the left (matches the built-in
|
|
// Glance calendar's "calendar-undo-button" entrance animation). Uses
|
|
// rAF-driven manual animation for reliability.
|
|
function animateUndoEntrance(undoEl) {
|
|
if (!undoEl || undoEl.hidden) return;
|
|
// translateX(-100%) means shift by the element's own width; use
|
|
// offsetWidth to compute the actual pixel distance.
|
|
var startX = -undoEl.offsetWidth;
|
|
var startTime = null;
|
|
function step(now) {
|
|
if (startTime === null) startTime = now;
|
|
var elapsed = now - startTime;
|
|
var progress = Math.min(elapsed / 400, 1);
|
|
var eased = 1 - Math.pow(1 - progress, 3);
|
|
undoEl.style.opacity = String(eased);
|
|
undoEl.style.transform = 'translateX(' + (startX * (1 - eased)) + 'px)';
|
|
if (progress < 1) {
|
|
requestAnimationFrame(step);
|
|
} else {
|
|
undoEl.style.opacity = '';
|
|
undoEl.style.transform = '';
|
|
}
|
|
}
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
function renderGrid(widget, year, month, byDate, today, todayParts) {
|
|
var monthEl = widget.querySelector('[data-arr-month]');
|
|
var yearEl = widget.querySelector('[data-arr-year]');
|
|
var monthNumEl = widget.querySelector('[data-arr-monthnum]');
|
|
var undoEl = widget.querySelector('[data-arr-undo]');
|
|
var gridEl = widget.querySelector('[data-arr-grid]');
|
|
|
|
monthEl.textContent = MONTH_NAMES[month];
|
|
yearEl.textContent = year;
|
|
monthNumEl.textContent = pad2(month + 1);
|
|
// Show undo button only when not viewing the current month
|
|
var onCurrent = (year === todayParts.y && month === todayParts.m);
|
|
undoEl.hidden = onCurrent;
|
|
|
|
gridEl.innerHTML = '';
|
|
var rows = buildRows(year, month);
|
|
|
|
for (var r = 0; r < rows.length; r++) {
|
|
var row = rows[r];
|
|
// Group weekday cells: render header row as plain text cells
|
|
if (row[0].type === 'weekday') {
|
|
for (var i = 0; i < 7; i++) {
|
|
var wd = document.createElement('div');
|
|
wd.className = 'arr-cal-weekday';
|
|
wd.textContent = row[i].text;
|
|
gridEl.appendChild(wd);
|
|
}
|
|
continue;
|
|
}
|
|
for (var j = 0; j < 7; j++) {
|
|
var cellData = row[j];
|
|
var cell = document.createElement('button');
|
|
cell.type = 'button';
|
|
cell.className = 'arr-cal-cell';
|
|
if (cellData.outsideMonth) {
|
|
cell.classList.add('arr-cal-outside');
|
|
} else {
|
|
var releases = byDate[cellData.iso] || [];
|
|
cellData.releases = releases;
|
|
cell.setAttribute('data-date', cellData.iso);
|
|
cell.setAttribute('aria-label', cellData.iso +
|
|
(releases.length > 0 ? ', ' + releases.length + ' releases' : ''));
|
|
if (cellData.iso === today) cell.classList.add('arr-cal-today');
|
|
if (releases.length > 0) cell.classList.add('arr-cal-has-releases');
|
|
}
|
|
|
|
var num = document.createElement('span');
|
|
num.className = 'arr-cal-num';
|
|
num.textContent = String(cellData.day);
|
|
cell.appendChild(num);
|
|
|
|
if (!cellData.outsideMonth && cellData.releases.length > 0) {
|
|
var dots = document.createElement('span');
|
|
dots.className = 'arr-cal-dots';
|
|
var seen = {};
|
|
for (var k = 0; k < cellData.releases.length; k++) {
|
|
var svc = cellData.releases[k].service;
|
|
if (seen[svc]) continue;
|
|
seen[svc] = true;
|
|
var dot = document.createElement('span');
|
|
dot.className = 'arr-dot arr-dot-' + svc;
|
|
dots.appendChild(dot);
|
|
}
|
|
cell.appendChild(dots);
|
|
|
|
(function (cd, c) {
|
|
c.addEventListener('click', function (ev) {
|
|
ev.stopPropagation();
|
|
showPopover(widget, c, cd.releases);
|
|
});
|
|
})(cellData, cell);
|
|
} else if (!cellData.outsideMonth) {
|
|
(function (c) {
|
|
c.addEventListener('click', function (ev) {
|
|
ev.stopPropagation();
|
|
hidePopover(widget);
|
|
});
|
|
})(cell);
|
|
}
|
|
|
|
gridEl.appendChild(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Position the popover relative to the viewport (position: fixed) so it
|
|
// can overlay any other widget on the page. The popover must live in
|
|
// document.body, NOT inside the widget, because the widget is wrapped by
|
|
// .widget-content (which has backdrop-filter: blur) — and backdrop-filter
|
|
// creates a containing block for position:fixed descendants, which would
|
|
// clip the popover to the widget. Moving it to <body> avoids that.
|
|
function positionPopover(pop, cell) {
|
|
var rect = cell.getBoundingClientRect();
|
|
var popRect = pop.getBoundingClientRect();
|
|
var margin = 8;
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
|
|
// Default: place below the cell, horizontally aligned to its left edge
|
|
var top = rect.bottom + margin;
|
|
var left = rect.left;
|
|
|
|
// If popover would overflow the bottom, place it above the cell
|
|
if (top + popRect.height > vh - margin) {
|
|
top = rect.top - popRect.height - margin;
|
|
}
|
|
// If still off-screen (cell near top), clamp to viewport
|
|
if (top < margin) top = margin;
|
|
|
|
// If popover would overflow the right edge, shift left
|
|
if (left + popRect.width > vw - margin) {
|
|
left = vw - popRect.width - margin;
|
|
}
|
|
if (left < margin) left = margin;
|
|
|
|
pop.style.position = 'fixed';
|
|
pop.style.top = top + 'px';
|
|
pop.style.left = left + 'px';
|
|
}
|
|
|
|
function showPopover(widget, cell, releases) {
|
|
var pop = findPopover(widget);
|
|
if (!pop) {
|
|
// No popover element found anywhere — should not happen since the
|
|
// widget template ships one, but be defensive.
|
|
return;
|
|
}
|
|
pop.innerHTML = '';
|
|
|
|
var header = document.createElement('div');
|
|
header.className = 'arr-popover-header';
|
|
var iso = cell.getAttribute('data-date');
|
|
var d = parseISODate(iso);
|
|
var headerDate = new Date(d.y, d.m, d.d);
|
|
var opts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
|
header.textContent = headerDate.toLocaleDateString(undefined, opts);
|
|
pop.appendChild(header);
|
|
|
|
if (releases.length === 0) {
|
|
var empty = document.createElement('div');
|
|
empty.className = 'arr-popover-empty';
|
|
empty.textContent = 'No releases on this day.';
|
|
pop.appendChild(empty);
|
|
} else {
|
|
var list = document.createElement('ul');
|
|
list.className = 'arr-popover-list';
|
|
releases.sort(function (a, b) {
|
|
return a.service.localeCompare(b.service);
|
|
});
|
|
for (var i = 0; i < releases.length; i++) {
|
|
var rel = releases[i];
|
|
var item = document.createElement('li');
|
|
item.className = 'arr-popover-item arr-svc-' + rel.service;
|
|
|
|
var cover = document.createElement('img');
|
|
cover.className = 'arr-popover-cover';
|
|
cover.loading = 'lazy';
|
|
cover.alt = '';
|
|
if (rel.cover) {
|
|
cover.src = rel.cover;
|
|
} else {
|
|
cover.style.display = 'none';
|
|
}
|
|
item.appendChild(cover);
|
|
|
|
var text = document.createElement('div');
|
|
text.className = 'arr-popover-text';
|
|
|
|
var titleEl = document.createElement('a');
|
|
titleEl.className = 'arr-popover-title';
|
|
titleEl.href = rel.link;
|
|
titleEl.target = '_blank';
|
|
titleEl.rel = 'noopener noreferrer';
|
|
titleEl.textContent = rel.title;
|
|
text.appendChild(titleEl);
|
|
|
|
var sub = document.createElement('div');
|
|
sub.className = 'arr-popover-sub';
|
|
var badge = document.createElement('span');
|
|
badge.className = 'arr-popover-badge arr-svc-badge-' + rel.service;
|
|
badge.textContent = rel.service;
|
|
sub.appendChild(badge);
|
|
var subText = document.createTextNode(' ' + rel.subtitle);
|
|
sub.appendChild(subText);
|
|
text.appendChild(sub);
|
|
|
|
item.appendChild(text);
|
|
list.appendChild(item);
|
|
}
|
|
pop.appendChild(list);
|
|
}
|
|
|
|
// Move the popover out of the widget to <body> so it can use
|
|
// position:fixed relative to the viewport. The widget is wrapped by
|
|
// .widget-content which has backdrop-filter, and backdrop-filter
|
|
// creates a containing block for position:fixed descendants.
|
|
if (pop.parentElement !== document.body) {
|
|
document.body.appendChild(pop);
|
|
}
|
|
// Tag the popover with its owning widget id so we can find it again
|
|
// after it's been moved (findPopover uses this).
|
|
pop.dataset.widgetId = widget.dataset.arrId;
|
|
|
|
// Make visible first (so getBoundingClientRect works), then position
|
|
pop.hidden = false;
|
|
pop.style.top = '0px';
|
|
pop.style.left = '0px';
|
|
// Force layout, then position relative to the clicked cell
|
|
positionPopover(pop, cell);
|
|
pop.dataset.date = iso;
|
|
}
|
|
|
|
function hidePopover(widget) {
|
|
var pop = findPopover(widget);
|
|
if (!pop) return;
|
|
pop.hidden = true;
|
|
pop.innerHTML = '';
|
|
delete pop.dataset.date;
|
|
// Move the popover back inside the widget so it stays associated with it
|
|
// (and so the next showPopover will find it there).
|
|
if (pop.parentElement === document.body) {
|
|
widget.appendChild(pop);
|
|
}
|
|
}
|
|
|
|
function initWidget(widget) {
|
|
ensureWidgetId(widget);
|
|
if (widget.dataset.arrInited === '1') {
|
|
// Already inited — but Dynacat's SSE may have re-morphed the widget
|
|
// HTML in place. In that case the cells were replaced and lost their
|
|
// click handlers. Detect a fresh grid (no children yet) and re-render.
|
|
var existingGrid = widget.querySelector('.arr-calendar-grid');
|
|
if (existingGrid && existingGrid.children.length > 0) return;
|
|
}
|
|
widget.dataset.arrInited = '1';
|
|
|
|
var today = widget.getAttribute('data-today') ||
|
|
formatISO(new Date().getFullYear(), new Date().getMonth(), new Date().getDate());
|
|
var todayParts = parseISODate(today);
|
|
var byDate = collectReleases(widget);
|
|
var view = { y: todayParts.y, m: todayParts.m };
|
|
|
|
function render() {
|
|
renderGrid(widget, view.y, view.m, byDate, today, todayParts);
|
|
}
|
|
|
|
// Track whether the undo button is currently visible, so we can decide
|
|
// whether to animate its entrance on the next render.
|
|
var undoWasVisible = false;
|
|
|
|
// Capture the current grid state for animation, then re-render with
|
|
// the new month and animate the change. The grid element is looked up
|
|
// FRESH on each call so that the animation applies to the live grid
|
|
// even if the widget was re-morphed by Dynacat's SSE update between
|
|
// page load and now (a closure-captured reference would be stale).
|
|
function navigateWithTransition(dir) {
|
|
if (dir !== -1) dir = 1;
|
|
var onCurrent = (view.y === todayParts.y && view.m === todayParts.m);
|
|
var undoWillBeVisible = !onCurrent;
|
|
|
|
animateGridChange(widget, function () {
|
|
render();
|
|
}, dir);
|
|
|
|
if (undoWillBeVisible && !undoWasVisible) {
|
|
// Wait a tick so the DOM updates before animating
|
|
setTimeout(function () {
|
|
animateUndoEntrance(widget.querySelector('[data-arr-undo]'));
|
|
}, 0);
|
|
}
|
|
undoWasVisible = undoWillBeVisible;
|
|
}
|
|
|
|
widget.querySelector('[data-arr-nav="prev"]').addEventListener('click', function () {
|
|
view.m -= 1;
|
|
if (view.m < 0) { view.m = 11; view.y -= 1; }
|
|
hidePopover(widget);
|
|
navigateWithTransition(-1);
|
|
});
|
|
widget.querySelector('[data-arr-nav="next"]').addEventListener('click', function () {
|
|
view.m += 1;
|
|
if (view.m > 11) { view.m = 0; view.y += 1; }
|
|
hidePopover(widget);
|
|
navigateWithTransition(1);
|
|
});
|
|
widget.querySelector('[data-arr-undo]').addEventListener('click', function () {
|
|
// Direction: if current view is in the past, undo = forward (+1).
|
|
// If in the future, undo = backward (-1).
|
|
var dir = 1;
|
|
if (view.y > todayParts.y || (view.y === todayParts.y && view.m > todayParts.m)) {
|
|
dir = -1;
|
|
}
|
|
view.y = todayParts.y;
|
|
view.m = todayParts.m;
|
|
hidePopover(widget);
|
|
navigateWithTransition(dir);
|
|
});
|
|
|
|
render();
|
|
undoWasVisible = false; // initial render, undo is hidden
|
|
}
|
|
|
|
// Find the popover element. It normally lives inside the widget, but
|
|
// during display it's moved to <body> to escape the widget's
|
|
// backdrop-filter containing block. Look in both places.
|
|
function findPopover(widget) {
|
|
var pop = widget.querySelector('[data-arr-popover]');
|
|
if (pop) return pop;
|
|
// The popover may be on <body>. Find it by its dataset/attribute.
|
|
var bodyPops = document.body.querySelectorAll('[data-arr-popover].arr-calendar-popover');
|
|
for (var i = 0; i < bodyPops.length; i++) {
|
|
var bp = bodyPops[i];
|
|
if (bp.dataset.widgetId === widget.dataset.arrId) return bp;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Assign a stable id to the widget so we can find its popover on <body>.
|
|
function ensureWidgetId(widget) {
|
|
if (!widget.dataset.arrId) {
|
|
widget.dataset.arrId = 'arrcal_' + Math.random().toString(36).slice(2, 10);
|
|
}
|
|
return widget.dataset.arrId;
|
|
}
|
|
|
|
// Close popover when clicking outside any arr-calendar-widget
|
|
document.addEventListener('click', function (ev) {
|
|
var widgets = document.querySelectorAll('.arr-calendar-widget');
|
|
for (var i = 0; i < widgets.length; i++) {
|
|
var w = widgets[i];
|
|
if (w.contains(ev.target)) continue;
|
|
var pop = findPopover(w);
|
|
if (pop && !pop.hidden) hidePopover(w);
|
|
}
|
|
});
|
|
|
|
// Esc closes popover
|
|
document.addEventListener('keydown', function (ev) {
|
|
if (ev.key !== 'Escape') return;
|
|
var widgets = document.querySelectorAll('.arr-calendar-widget');
|
|
for (var i = 0; i < widgets.length; i++) {
|
|
hidePopover(widgets[i]);
|
|
}
|
|
});
|
|
|
|
// Reposition popover on scroll/resize so it stays attached to its cell
|
|
window.addEventListener('scroll', function () {
|
|
var widgets = document.querySelectorAll('.arr-calendar-widget');
|
|
for (var i = 0; i < widgets.length; i++) {
|
|
var widget = widgets[i];
|
|
var pop = findPopover(widget);
|
|
if (!pop || pop.hidden) continue;
|
|
var iso = pop.dataset.date;
|
|
if (!iso) continue;
|
|
var cell = widget.querySelector('.arr-cal-cell[data-date="' + iso + '"]');
|
|
if (cell) positionPopover(pop, cell);
|
|
}
|
|
}, true);
|
|
window.addEventListener('resize', function () {
|
|
var widgets = document.querySelectorAll('.arr-calendar-widget');
|
|
for (var i = 0; i < widgets.length; i++) {
|
|
var widget = widgets[i];
|
|
var pop = findPopover(widget);
|
|
if (!pop || pop.hidden) continue;
|
|
var iso = pop.dataset.date;
|
|
if (!iso) continue;
|
|
var cell = widget.querySelector('.arr-cal-cell[data-date="' + iso + '"]');
|
|
if (cell) positionPopover(pop, cell);
|
|
}
|
|
});
|
|
|
|
// Boot: scan DOM now, then watch for widgets added later (Dynacat's
|
|
// custom-api widgets are inserted via innerHTML/XHR after page load,
|
|
// which means our <script src="arr-calendar.js"> tag inside the widget
|
|
// is never executed, and the initial scan() finds zero widgets).
|
|
function scan() {
|
|
var widgets = document.querySelectorAll('.arr-calendar-widget:not([data-arr-inited])');
|
|
for (var i = 0; i < widgets.length; i++) {
|
|
initWidget(widgets[i]);
|
|
}
|
|
}
|
|
|
|
function startObserver() {
|
|
scan();
|
|
var observer = new MutationObserver(function (mutations) {
|
|
var found = false;
|
|
for (var i = 0; i < mutations.length; i++) {
|
|
var added = mutations[i].addedNodes;
|
|
for (var j = 0; j < added.length; j++) {
|
|
var n = added[j];
|
|
if (n.nodeType !== 1) continue;
|
|
if (n.classList && n.classList.contains('arr-calendar-widget')) {
|
|
initWidget(n);
|
|
found = true;
|
|
} else if (n.querySelectorAll) {
|
|
var inner = n.querySelectorAll('.arr-calendar-widget');
|
|
for (var k = 0; k < inner.length; k++) {
|
|
initWidget(inner[k]);
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!found) scan();
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
|
|
if (document.body) {
|
|
startObserver();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', startObserver);
|
|
}
|
|
})();
|