Initial release: arr-calendar widget with partial-services support
- 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
This commit is contained in:
+634
@@ -0,0 +1,634 @@
|
||||
(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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user