@tasknotes/calendar 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +31 -0
- package/dist/calendar.css +53 -0
- package/dist/cjs/index.cjs +190 -30
- package/dist/cjs/react.cjs +190 -30
- package/dist/esm/{chunk-URE7QLU3.js → chunk-EDRCVOY7.js} +190 -30
- package/dist/esm/index.js +1 -1
- package/dist/esm/react.js +1 -1
- package/dist/types/dom/calendar.d.cts +5 -1
- package/dist/types/dom/calendar.d.ts +5 -1
- package/dist/types/dom/context.d.cts +1 -1
- package/dist/types/dom/context.d.ts +1 -1
- package/dist/types/dom/options.d.cts +3 -0
- package/dist/types/dom/options.d.ts +3 -0
- package/dist/types/dom/views/timegrid.d.cts +4 -0
- package/dist/types/dom/views/timegrid.d.ts +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.7
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Hosts can read collision column counts and the unobscured timed content width, choose views responsively, fill an empty list through a content hook, and translate the stack label with the `appointments` text.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Collision stacks show appointment counts and open only the matching collision group, without unrelated all-day or timed items. Narrow time grids reveal the selected day on entry and view changes, not only after Today.
|
|
14
|
+
- Crowded time-grid days use readable collision stacks without widening the entire week; stack contents retain drag-and-drop, including after moving the calendar into another window.
|
|
15
|
+
- Today reveals the current day horizontally, with persistent day navigation when the grid is wider than its pane.
|
|
16
|
+
- Overflow dialogs clamp to the visible viewport, reposition on resize and scroll, keep Close reachable, and provide touch-sized rows and keyboard navigation.
|
|
17
|
+
- Collision stacks honour `eventMaxStack`, and overflow triggers stay stable across rerenders, so Escape returns focus to them.
|
|
18
|
+
|
|
3
19
|
## 0.1.6
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -69,8 +69,15 @@ to 00:00–24:00 in 30-minute slots, initially scrolled to 08:00.
|
|
|
69
69
|
`text` supplies translated labels; `itemContent` and related hooks supply
|
|
70
70
|
custom DOM. The exported `CalendarOptions<D>` type documents the full API.
|
|
71
71
|
|
|
72
|
+
`toolbar.viewDisplay` defaults to `"auto"`: full view-name buttons become a
|
|
73
|
+
native select when the toolbar cannot fit them. Use `"buttons"` or `"select"`
|
|
74
|
+
to choose a fixed presentation. The select uses the same translated view names
|
|
75
|
+
and changes views through the same navigation lifecycle as the buttons.
|
|
76
|
+
|
|
72
77
|
### Month and list
|
|
73
78
|
|
|
79
|
+
- `listEmptyContent()` may return a host-owned DOM node for an empty list, such as an explanation and recovery action. The default translated empty message remains when omitted. The node is removed when the list gains items.
|
|
80
|
+
|
|
74
81
|
- `monthDisplay: "dots"` draws every item in a month cell as a small dot (`--tnc-dot-size`, `--tnc-dot-gap`) that wraps across the cell, with "+N" when they do not fit; `dayMaxEvents` caps the dots (`false` lets cells grow). The day cell is the tap target: dots ignore the pointer, are `aria-hidden`, get no content hook, and the cell is labelled "<date>, N items" (`text.itemCount`). Items in dots have `variant: "indicator"`; colour them with `color` or `itemDidMount` setting `--tnc-c`.
|
|
75
82
|
- **"+N more" height**: day rows (and the time grid's all-day row) make room for the link at its rendered height, so a host may give it a 44px touch target (`--tnc-more-h`, or any min-height) without overflowing the row.
|
|
76
83
|
- `listStart: "date" | "week"`: the list view starts at the date (default; FullCalendar `duration: { days: n }` lists, as the TaskNotes plugin and app used) or at the start of its week (FullCalendar's stock `listWeek`); week-aligned lists step by whole weeks.
|
|
@@ -105,6 +112,30 @@ custom DOM. The exported `CalendarOptions<D>` type documents the full API.
|
|
|
105
112
|
Set `--tnc-theme-*` variables on the host (they inherit into the calendar)
|
|
106
113
|
or on `.tnc`. The stylesheet is scoped to `.tnc` and does not inject CSS.
|
|
107
114
|
|
|
115
|
+
Time grids collapse crowded collision groups into labelled, expandable stacks
|
|
116
|
+
instead of widening every day. `--tnc-event-min-width` sets the readable event
|
|
117
|
+
width in pixels (default 128). Keep the day minimum independent of collisions:
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
.my-calendar .tnc-tg {
|
|
121
|
+
--tnc-col-min: 128px;
|
|
122
|
+
--tnc-event-min-width: 128px;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When the day minimums exceed a narrow viewport, persistent day buttons reveal
|
|
127
|
+
offscreen dates, and Today reveals the current day. Headers, all-day rows and
|
|
128
|
+
timed columns stay aligned. `--tnc-collision-columns` remains available as
|
|
129
|
+
diagnostic metadata (respecting `eventMaxStack`), not a week-wide width multiplier.
|
|
130
|
+
Overflow dialogs clamp to the visible calendar and viewport, scroll vertically,
|
|
131
|
+
and support Tab, arrow keys, Home, End and Escape. Timed items dragged from
|
|
132
|
+
overflow start at their actual time rather than the slot underneath the dialog.
|
|
133
|
+
|
|
134
|
+
Timed events also expose `--tnc-event-readable-width`, the percentage of their
|
|
135
|
+
width not covered by the next collision column. Hosts can use it as the
|
|
136
|
+
`max-width` of `.tnc-item-main` to keep text readable while preserving the user's
|
|
137
|
+
overlapping event backgrounds. It is `100%` for non-overlapping placements.
|
|
138
|
+
|
|
108
139
|
```css
|
|
109
140
|
.my-calendar {
|
|
110
141
|
--tnc-theme-bg: #181818;
|
package/dist/calendar.css
CHANGED
|
@@ -467,6 +467,35 @@
|
|
|
467
467
|
overflow: auto;
|
|
468
468
|
overscroll-behavior: contain;
|
|
469
469
|
}
|
|
470
|
+
.tnc-tg-day-navigation {
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-wrap: wrap;
|
|
473
|
+
gap: 2px;
|
|
474
|
+
flex: none;
|
|
475
|
+
}
|
|
476
|
+
.tnc-tg-day-navigation[hidden] {
|
|
477
|
+
display: none;
|
|
478
|
+
}
|
|
479
|
+
.tnc-tg-day-navigation .is-offscreen {
|
|
480
|
+
text-decoration: underline;
|
|
481
|
+
text-underline-offset: 3px;
|
|
482
|
+
}
|
|
483
|
+
.tnc-more--collision {
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: start;
|
|
486
|
+
gap: 4px;
|
|
487
|
+
min-height: 40px;
|
|
488
|
+
white-space: normal;
|
|
489
|
+
text-align: start;
|
|
490
|
+
}
|
|
491
|
+
.tnc-collision-title {
|
|
492
|
+
flex: 1;
|
|
493
|
+
min-width: 0;
|
|
494
|
+
display: -webkit-box;
|
|
495
|
+
-webkit-box-orient: vertical;
|
|
496
|
+
-webkit-line-clamp: 3;
|
|
497
|
+
overflow: hidden;
|
|
498
|
+
}
|
|
470
499
|
.tnc--height-auto .tnc-tg-scroll {
|
|
471
500
|
overflow: visible;
|
|
472
501
|
}
|
|
@@ -1620,6 +1649,7 @@
|
|
|
1620
1649
|
outline: none;
|
|
1621
1650
|
}
|
|
1622
1651
|
.tnc-popover-head {
|
|
1652
|
+
flex: none;
|
|
1623
1653
|
display: flex;
|
|
1624
1654
|
align-items: center;
|
|
1625
1655
|
justify-content: space-between;
|
|
@@ -1629,6 +1659,7 @@
|
|
|
1629
1659
|
font-weight: 650;
|
|
1630
1660
|
}
|
|
1631
1661
|
.tnc-popover-close {
|
|
1662
|
+
flex: none;
|
|
1632
1663
|
min-width: 28px;
|
|
1633
1664
|
min-height: 28px;
|
|
1634
1665
|
border: 0;
|
|
@@ -1642,6 +1673,7 @@
|
|
|
1642
1673
|
background: var(--tnc-surface-3);
|
|
1643
1674
|
}
|
|
1644
1675
|
.tnc-popover-body {
|
|
1676
|
+
min-height: 0;
|
|
1645
1677
|
display: flex;
|
|
1646
1678
|
flex-direction: column;
|
|
1647
1679
|
gap: 2px;
|
|
@@ -1652,6 +1684,27 @@
|
|
|
1652
1684
|
position: relative;
|
|
1653
1685
|
flex: none;
|
|
1654
1686
|
}
|
|
1687
|
+
.tnc-popover-title {
|
|
1688
|
+
min-width: 0;
|
|
1689
|
+
overflow: hidden;
|
|
1690
|
+
text-overflow: ellipsis;
|
|
1691
|
+
white-space: nowrap;
|
|
1692
|
+
}
|
|
1693
|
+
.tnc-popover-body .tnc-item {
|
|
1694
|
+
height: auto;
|
|
1695
|
+
min-height: 40px;
|
|
1696
|
+
}
|
|
1697
|
+
.tnc-popover-body .tnc-item-main {
|
|
1698
|
+
min-height: 40px;
|
|
1699
|
+
}
|
|
1700
|
+
.tnc-popover-body .tnc-item-title {
|
|
1701
|
+
white-space: normal;
|
|
1702
|
+
overflow-wrap: anywhere;
|
|
1703
|
+
}
|
|
1704
|
+
.tnc-popover-close:focus-visible {
|
|
1705
|
+
outline: 2px solid var(--tnc-accent);
|
|
1706
|
+
outline-offset: -2px;
|
|
1707
|
+
}
|
|
1655
1708
|
|
|
1656
1709
|
@media (prefers-reduced-motion: reduce) {
|
|
1657
1710
|
.tnc * {
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -953,6 +953,7 @@ var DEFAULT_TEXT = {
|
|
|
953
953
|
allDay: "All day",
|
|
954
954
|
noItems: "Nothing scheduled",
|
|
955
955
|
more: (n) => `+${n} more`,
|
|
956
|
+
appointments: (n) => `${n} appointments`,
|
|
956
957
|
itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
|
|
957
958
|
weekNumber: (n) => `W${n}`,
|
|
958
959
|
views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
|
|
@@ -1022,6 +1023,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
|
|
|
1022
1023
|
"dateStrip",
|
|
1023
1024
|
"listLeadTitle",
|
|
1024
1025
|
"listAddText",
|
|
1026
|
+
"listEmptyContent",
|
|
1025
1027
|
"monthDisplay",
|
|
1026
1028
|
"timeZone",
|
|
1027
1029
|
"locale",
|
|
@@ -1261,19 +1263,27 @@ var DayGridView = class {
|
|
|
1261
1263
|
});
|
|
1262
1264
|
}
|
|
1263
1265
|
w.pool.end();
|
|
1264
|
-
w.more.
|
|
1266
|
+
const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
|
|
1265
1267
|
hidden.forEach((items, col) => {
|
|
1266
1268
|
if (!items.length) return;
|
|
1267
1269
|
const day = w.days[col];
|
|
1268
|
-
const
|
|
1270
|
+
const key = String(day);
|
|
1271
|
+
let link = existing.get(key);
|
|
1272
|
+
if (!link) {
|
|
1273
|
+
link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
|
|
1274
|
+
const anchor = link;
|
|
1275
|
+
link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
|
|
1276
|
+
w.more.append(link);
|
|
1277
|
+
}
|
|
1278
|
+
existing.delete(key);
|
|
1279
|
+
link.textContent = ctx.text.more(items.length);
|
|
1269
1280
|
setStyle(link, {
|
|
1270
1281
|
left: `calc(${pct(col / n)} + 2px)`,
|
|
1271
1282
|
width: `calc(${pct(1 / n)} - 4px)`,
|
|
1272
1283
|
top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
|
|
1273
1284
|
});
|
|
1274
|
-
link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
|
|
1275
|
-
w.more.append(link);
|
|
1276
1285
|
});
|
|
1286
|
+
for (const link of existing.values()) link.remove();
|
|
1277
1287
|
const step = this.barPx + BAR_GAP;
|
|
1278
1288
|
let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
|
|
1279
1289
|
hidden.forEach((items, col) => {
|
|
@@ -1476,6 +1486,8 @@ var ListView = class {
|
|
|
1476
1486
|
if (!keep.size && !leadItems.length) {
|
|
1477
1487
|
if (!empty) {
|
|
1478
1488
|
empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
|
|
1489
|
+
const content = ctx.opts.listEmptyContent?.();
|
|
1490
|
+
if (content) empty.replaceChildren(content);
|
|
1479
1491
|
this.body.append(empty);
|
|
1480
1492
|
}
|
|
1481
1493
|
} else empty?.remove();
|
|
@@ -1824,6 +1836,8 @@ var TimeGridView = class {
|
|
|
1824
1836
|
this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
|
|
1825
1837
|
this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
|
|
1826
1838
|
this.scroller = h(doc, "div", "tnc-tg-scroll");
|
|
1839
|
+
this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
|
|
1840
|
+
this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
|
|
1827
1841
|
this.top = h(doc, "div", "tnc-tg-top");
|
|
1828
1842
|
this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
|
|
1829
1843
|
this.top.append(this.headRow);
|
|
@@ -1851,7 +1865,7 @@ var TimeGridView = class {
|
|
|
1851
1865
|
} else {
|
|
1852
1866
|
this.body = this.gutter = null;
|
|
1853
1867
|
}
|
|
1854
|
-
this.el.append(this.scroller);
|
|
1868
|
+
this.el.append(this.dayNavigation, this.scroller);
|
|
1855
1869
|
this.build();
|
|
1856
1870
|
if (showGrid && opts.height === "fill") {
|
|
1857
1871
|
const RO = ctx.win.ResizeObserver;
|
|
@@ -1864,6 +1878,7 @@ var TimeGridView = class {
|
|
|
1864
1878
|
ctx;
|
|
1865
1879
|
el;
|
|
1866
1880
|
scroller;
|
|
1881
|
+
dayNavigation;
|
|
1867
1882
|
top;
|
|
1868
1883
|
headRow;
|
|
1869
1884
|
allDayRow;
|
|
@@ -1919,6 +1934,9 @@ var TimeGridView = class {
|
|
|
1919
1934
|
head.append(cap);
|
|
1920
1935
|
}
|
|
1921
1936
|
this.headRow.append(head);
|
|
1937
|
+
const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
|
|
1938
|
+
dayButton.addEventListener("click", () => this.revealDay(day));
|
|
1939
|
+
this.dayNavigation.append(dayButton);
|
|
1922
1940
|
opts.dayHeaderDidMount?.({ ...arg, el: head });
|
|
1923
1941
|
const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
|
|
1924
1942
|
const bgLayer = h(doc, "div", "tnc-col-bg");
|
|
@@ -1935,6 +1953,7 @@ var TimeGridView = class {
|
|
|
1935
1953
|
this.body?.append(col);
|
|
1936
1954
|
return {
|
|
1937
1955
|
day,
|
|
1956
|
+
collisionColumns: 1,
|
|
1938
1957
|
head,
|
|
1939
1958
|
col,
|
|
1940
1959
|
plan: new ItemPool(ctx, planLayer),
|
|
@@ -1991,12 +2010,17 @@ var TimeGridView = class {
|
|
|
1991
2010
|
/* --------------------------------------------------------------- render */
|
|
1992
2011
|
render(change) {
|
|
1993
2012
|
this.renderAllDay();
|
|
1994
|
-
if (!this.body)
|
|
2013
|
+
if (!this.body) {
|
|
2014
|
+
this.updateDayNavigation();
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
1995
2017
|
const dirty = change ? change.days : null;
|
|
1996
2018
|
for (const c of this.cols) {
|
|
1997
2019
|
if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
|
|
1998
2020
|
this.renderColumn(c);
|
|
1999
2021
|
}
|
|
2022
|
+
this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
|
|
2023
|
+
this.updateDayNavigation();
|
|
2000
2024
|
this.renderCapacity();
|
|
2001
2025
|
this.tick();
|
|
2002
2026
|
}
|
|
@@ -2114,6 +2138,11 @@ var TimeGridView = class {
|
|
|
2114
2138
|
}
|
|
2115
2139
|
const minMinutes = opts.eventMinHeight / this.ppm;
|
|
2116
2140
|
const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
|
|
2141
|
+
c.collisionColumns = Math.max(
|
|
2142
|
+
1,
|
|
2143
|
+
...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
|
|
2144
|
+
...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
|
|
2145
|
+
);
|
|
2117
2146
|
const total = this.minutes;
|
|
2118
2147
|
const top = (m) => pct((m - slot.min) / total);
|
|
2119
2148
|
const height = (s) => pct((s.endMin - s.startMin) / total);
|
|
@@ -2125,14 +2154,22 @@ var TimeGridView = class {
|
|
|
2125
2154
|
c.bg.end();
|
|
2126
2155
|
const hasActual = layout.actual.length > 0;
|
|
2127
2156
|
c.col.classList.toggle("has-actual", hasActual);
|
|
2157
|
+
for (const link of c.more.children) link.setAttribute("data-stale", "true");
|
|
2128
2158
|
this.placeLane(c.plan, layout.plan, c, top, height, "plan");
|
|
2129
2159
|
this.placeLane(c.actual, layout.actual, c, top, height, "actual");
|
|
2160
|
+
for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
|
|
2130
2161
|
}
|
|
2131
2162
|
placeLane(pool, placements, c, top, height, lane) {
|
|
2132
2163
|
const { ctx } = this;
|
|
2133
2164
|
const { opts } = ctx;
|
|
2134
|
-
const
|
|
2135
|
-
const
|
|
2165
|
+
const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
|
|
2166
|
+
const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
|
|
2167
|
+
const collapse = placements.some((placement) => {
|
|
2168
|
+
const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
|
|
2169
|
+
return columns > 1 && columns * readableWidth > laneWidth;
|
|
2170
|
+
});
|
|
2171
|
+
const withMore = lane === "plan" || collapse;
|
|
2172
|
+
const maxStack = collapse ? 1 : opts.eventMaxStack;
|
|
2136
2173
|
const hiddenByCluster = /* @__PURE__ */ new Map();
|
|
2137
2174
|
pool.begin();
|
|
2138
2175
|
for (const p of placements) {
|
|
@@ -2158,21 +2195,38 @@ var TimeGridView = class {
|
|
|
2158
2195
|
top: top(p.startMin),
|
|
2159
2196
|
height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
|
|
2160
2197
|
...inLane(lane, p.split, left, width),
|
|
2198
|
+
"--tnc-event-readable-width": pct(span / cols / width),
|
|
2161
2199
|
"z-index": String(p.col + 1)
|
|
2162
2200
|
});
|
|
2163
2201
|
}
|
|
2164
2202
|
pool.end();
|
|
2165
2203
|
if (!withMore) return;
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2204
|
+
for (const [cluster, rec] of hiddenByCluster) {
|
|
2205
|
+
const key = `${lane}-${cluster}`;
|
|
2206
|
+
let link = c.more.querySelector(`[data-cluster="${key}"]`);
|
|
2207
|
+
if (!link) {
|
|
2208
|
+
link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
|
|
2209
|
+
const anchor = link;
|
|
2210
|
+
link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
|
|
2211
|
+
c.more.append(link);
|
|
2212
|
+
}
|
|
2213
|
+
link.removeAttribute("data-stale");
|
|
2214
|
+
link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
|
|
2215
|
+
link.classList.toggle("tnc-more--collision", collapse);
|
|
2216
|
+
link.textContent = `+${rec.count}`;
|
|
2217
|
+
if (collapse) {
|
|
2218
|
+
link.textContent = ctx.text.appointments(rec.count);
|
|
2219
|
+
link.setAttribute("aria-label", link.textContent);
|
|
2220
|
+
link.title = link.getAttribute("aria-label");
|
|
2221
|
+
} else {
|
|
2222
|
+
link.removeAttribute("aria-label");
|
|
2223
|
+
link.removeAttribute("title");
|
|
2224
|
+
}
|
|
2169
2225
|
const cols = maxStack;
|
|
2170
2226
|
setStyle(link, {
|
|
2171
2227
|
top: top(rec.start),
|
|
2172
2228
|
...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
|
|
2173
2229
|
});
|
|
2174
|
-
link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
|
|
2175
|
-
c.more.append(link);
|
|
2176
2230
|
}
|
|
2177
2231
|
}
|
|
2178
2232
|
meta(s, bg) {
|
|
@@ -2283,6 +2337,26 @@ var TimeGridView = class {
|
|
|
2283
2337
|
}
|
|
2284
2338
|
resize() {
|
|
2285
2339
|
this.fitRows();
|
|
2340
|
+
this.render(null);
|
|
2341
|
+
}
|
|
2342
|
+
revealDay(day) {
|
|
2343
|
+
const column = this.cols.find((candidate) => candidate.day === day);
|
|
2344
|
+
if (!column) return;
|
|
2345
|
+
const bounds = column.head.getBoundingClientRect();
|
|
2346
|
+
const viewport = this.scroller.getBoundingClientRect();
|
|
2347
|
+
this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
|
|
2348
|
+
this.updateDayNavigation();
|
|
2349
|
+
}
|
|
2350
|
+
updateDayNavigation() {
|
|
2351
|
+
this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
|
|
2352
|
+
const viewport = this.scroller.getBoundingClientRect();
|
|
2353
|
+
this.cols.forEach((column, index) => {
|
|
2354
|
+
const bounds = column.head.getBoundingClientRect();
|
|
2355
|
+
const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
|
|
2356
|
+
const button = this.dayNavigation.children[index];
|
|
2357
|
+
button?.classList.toggle("is-offscreen", !visible);
|
|
2358
|
+
button?.setAttribute("aria-pressed", String(visible));
|
|
2359
|
+
});
|
|
2286
2360
|
}
|
|
2287
2361
|
scrollToTime(minute) {
|
|
2288
2362
|
if (!this.body) return;
|
|
@@ -3048,7 +3122,11 @@ var Calendar = class {
|
|
|
3048
3122
|
this.resizeObs?.disconnect();
|
|
3049
3123
|
const RO = this.win.ResizeObserver;
|
|
3050
3124
|
if (RO) {
|
|
3051
|
-
this.resizeObs = new RO(() =>
|
|
3125
|
+
this.resizeObs = new RO(() => {
|
|
3126
|
+
this.syncToolbarViewDisplay();
|
|
3127
|
+
this.current?.resize?.();
|
|
3128
|
+
this.positionMore();
|
|
3129
|
+
});
|
|
3052
3130
|
this.resizeObs.observe(this.root);
|
|
3053
3131
|
}
|
|
3054
3132
|
}
|
|
@@ -3068,9 +3146,11 @@ var Calendar = class {
|
|
|
3068
3146
|
if (this.frame !== null) old.cancelAnimationFrame(this.frame);
|
|
3069
3147
|
this.timer = this.frame = null;
|
|
3070
3148
|
this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
|
|
3149
|
+
this.listenMoreGeometry(false);
|
|
3071
3150
|
this.doc = doc;
|
|
3072
3151
|
this.win = doc.defaultView ?? old;
|
|
3073
3152
|
this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
|
|
3153
|
+
this.listenMoreGeometry(true);
|
|
3074
3154
|
this.observeResize();
|
|
3075
3155
|
this.rebuild(false);
|
|
3076
3156
|
this.scheduleTick();
|
|
@@ -3188,6 +3268,7 @@ var Calendar = class {
|
|
|
3188
3268
|
goToday() {
|
|
3189
3269
|
this.anchor = this.todayDay();
|
|
3190
3270
|
this.rebuild(true);
|
|
3271
|
+
if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
|
|
3191
3272
|
}
|
|
3192
3273
|
/** The anchor date, as local midnight of that civil day in the calendar zone. */
|
|
3193
3274
|
getDate() {
|
|
@@ -3206,6 +3287,7 @@ var Calendar = class {
|
|
|
3206
3287
|
/** Re-measures (e.g. after the host was shown or resized by a parent). */
|
|
3207
3288
|
updateSize() {
|
|
3208
3289
|
this.current?.resize?.();
|
|
3290
|
+
this.positionMore();
|
|
3209
3291
|
}
|
|
3210
3292
|
getScroll() {
|
|
3211
3293
|
return this.current?.getScroll() ?? { top: 0, left: 0 };
|
|
@@ -3237,7 +3319,10 @@ var Calendar = class {
|
|
|
3237
3319
|
}
|
|
3238
3320
|
const view = this.current;
|
|
3239
3321
|
if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
|
|
3240
|
-
else if (view instanceof TimeGridView
|
|
3322
|
+
else if (view instanceof TimeGridView) {
|
|
3323
|
+
view.revealDay(day);
|
|
3324
|
+
if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
|
|
3325
|
+
}
|
|
3241
3326
|
}
|
|
3242
3327
|
/** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
|
|
3243
3328
|
hitTest(clientX, clientY) {
|
|
@@ -3412,9 +3497,10 @@ var Calendar = class {
|
|
|
3412
3497
|
if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
|
|
3413
3498
|
el.remove();
|
|
3414
3499
|
}
|
|
3415
|
-
openMore(day, anchor, jsEvent) {
|
|
3500
|
+
openMore(day, anchor, jsEvent, itemIds) {
|
|
3416
3501
|
jsEvent.stopPropagation();
|
|
3417
|
-
const
|
|
3502
|
+
const group = itemIds ? new Set(itemIds) : null;
|
|
3503
|
+
const items = this.store.inRange(day, day + 1).filter((item) => !group || group.has(item.id)).filter((n) => n.item.display !== "background" && (n.allDay || anchor.classList.contains("tnc-more--stack") || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
|
|
3418
3504
|
const shown = new Set(
|
|
3419
3505
|
[...this.viewHost.querySelectorAll(".tnc-item")].filter((el2) => this.meta.get(el2)?.day === day || (this.meta.get(el2)?.n.startDay ?? Infinity) <= day).map((el2) => el2.dataset.id ?? "")
|
|
3420
3506
|
);
|
|
@@ -3443,15 +3529,7 @@ var Calendar = class {
|
|
|
3443
3529
|
}
|
|
3444
3530
|
pool.end();
|
|
3445
3531
|
this.popover = { el, pool, anchor, day };
|
|
3446
|
-
|
|
3447
|
-
const ar = anchor.getBoundingClientRect();
|
|
3448
|
-
const w = Math.max(220, ar.width + 24);
|
|
3449
|
-
let left = ar.left - rr.left - 12;
|
|
3450
|
-
left = Math.max(4, Math.min(left, rr.width - w - 4));
|
|
3451
|
-
let top = ar.top - rr.top - 40;
|
|
3452
|
-
const height = el.offsetHeight;
|
|
3453
|
-
top = Math.max(4, Math.min(top, rr.height - height - 4));
|
|
3454
|
-
setStyle(el, { left, top, width: w });
|
|
3532
|
+
this.positionMore();
|
|
3455
3533
|
el.focus();
|
|
3456
3534
|
}
|
|
3457
3535
|
/* =============================================================== render */
|
|
@@ -3575,8 +3653,10 @@ var Calendar = class {
|
|
|
3575
3653
|
this.current.setScroll({ top: keep.top, left: 0 });
|
|
3576
3654
|
this.current.placeInstant(keep.ms, keep.vx);
|
|
3577
3655
|
} else if (scroll) this.current.setScroll(scroll);
|
|
3578
|
-
else if (this.current instanceof TimeGridView)
|
|
3579
|
-
|
|
3656
|
+
else if (this.current instanceof TimeGridView) {
|
|
3657
|
+
this.current.scrollToTime(this.slot.scroll);
|
|
3658
|
+
this.current.revealDay(this.anchor);
|
|
3659
|
+
} else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
|
|
3580
3660
|
if (rangeChanged) this.opts.onRangeChange?.(this.view);
|
|
3581
3661
|
}
|
|
3582
3662
|
/** Sets the visible days and view info from the anchor. */
|
|
@@ -3747,17 +3827,42 @@ var Calendar = class {
|
|
|
3747
3827
|
);
|
|
3748
3828
|
const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
|
|
3749
3829
|
const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
|
|
3830
|
+
const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
|
|
3831
|
+
"aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
|
|
3832
|
+
});
|
|
3750
3833
|
for (const v of views) {
|
|
3751
3834
|
const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
|
|
3752
3835
|
viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
|
|
3836
|
+
viewSelect.append(h(this.doc, "option", "", { value: v }, label));
|
|
3753
3837
|
}
|
|
3838
|
+
viewSelect.value = this.opts.view;
|
|
3839
|
+
viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
|
|
3754
3840
|
const start = h(this.doc, "div", "tnc-toolbar-start");
|
|
3755
3841
|
const end = h(this.doc, "div", "tnc-toolbar-end");
|
|
3756
3842
|
if (cfg.extraPlacement === "start") start.append(...extras);
|
|
3757
3843
|
start.append(nav, title);
|
|
3758
|
-
end.append(viewGroup);
|
|
3844
|
+
end.append(viewGroup, viewSelect);
|
|
3759
3845
|
if (cfg.extraPlacement !== "start") end.append(...extras);
|
|
3760
3846
|
this.toolbarEl.append(start, end);
|
|
3847
|
+
this.syncToolbarViewDisplay();
|
|
3848
|
+
}
|
|
3849
|
+
syncToolbarViewDisplay() {
|
|
3850
|
+
const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
|
|
3851
|
+
const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
|
|
3852
|
+
const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
|
|
3853
|
+
if (!buttons || !select || !start) return;
|
|
3854
|
+
const config = this.opts.toolbar;
|
|
3855
|
+
const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
|
|
3856
|
+
buttons.hidden = false;
|
|
3857
|
+
select.hidden = true;
|
|
3858
|
+
buttons.style.display = "flex";
|
|
3859
|
+
select.style.display = "none";
|
|
3860
|
+
const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
|
|
3861
|
+
const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
|
|
3862
|
+
buttons.hidden = compact;
|
|
3863
|
+
select.hidden = !compact;
|
|
3864
|
+
buttons.style.display = compact ? "none" : "flex";
|
|
3865
|
+
select.style.display = compact ? "" : "none";
|
|
3761
3866
|
}
|
|
3762
3867
|
applyChange(change) {
|
|
3763
3868
|
if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
|
|
@@ -3890,6 +3995,38 @@ var Calendar = class {
|
|
|
3890
3995
|
}
|
|
3891
3996
|
}
|
|
3892
3997
|
/* ============================================================== popover */
|
|
3998
|
+
onMoreGeometryChange = () => this.positionMore();
|
|
3999
|
+
listenMoreGeometry(on) {
|
|
4000
|
+
const method = on ? "addEventListener" : "removeEventListener";
|
|
4001
|
+
this.doc[method]("scroll", this.onMoreGeometryChange, true);
|
|
4002
|
+
this.win[method]("resize", this.onMoreGeometryChange);
|
|
4003
|
+
this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
|
|
4004
|
+
this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
|
|
4005
|
+
}
|
|
4006
|
+
positionMore() {
|
|
4007
|
+
const popover = this.popover;
|
|
4008
|
+
if (!popover) return;
|
|
4009
|
+
if (!popover.anchor.isConnected) {
|
|
4010
|
+
this.closeMore();
|
|
4011
|
+
return;
|
|
4012
|
+
}
|
|
4013
|
+
const viewport = this.win.visualViewport;
|
|
4014
|
+
const root = this.root.getBoundingClientRect();
|
|
4015
|
+
const anchor = popover.anchor.getBoundingClientRect();
|
|
4016
|
+
const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
|
|
4017
|
+
const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
|
|
4018
|
+
const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
|
|
4019
|
+
const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
|
|
4020
|
+
if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
|
|
4021
|
+
this.closeMore();
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
const width = Math.min(360, rightEdge - leftEdge);
|
|
4025
|
+
setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
|
|
4026
|
+
const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
|
|
4027
|
+
const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
|
|
4028
|
+
setStyle(popover.el, { left: left - root.left, top: top - root.top });
|
|
4029
|
+
}
|
|
3893
4030
|
refreshMore() {
|
|
3894
4031
|
const p = this.popover;
|
|
3895
4032
|
if (!p) return;
|
|
@@ -3899,6 +4036,7 @@ var Calendar = class {
|
|
|
3899
4036
|
p.pool.place(n.id, { n, day: p.day, isStart: n.startDay >= p.day, isEnd: n.endDay <= p.day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
|
|
3900
4037
|
}
|
|
3901
4038
|
p.pool.end();
|
|
4039
|
+
this.positionMore();
|
|
3902
4040
|
}
|
|
3903
4041
|
closeMore(restoreFocus = false) {
|
|
3904
4042
|
const p = this.popover;
|
|
@@ -3926,6 +4064,7 @@ var Calendar = class {
|
|
|
3926
4064
|
r.addEventListener("pointerover", this.onAdoptCheck);
|
|
3927
4065
|
r.addEventListener("focusin", this.onAdoptCheck);
|
|
3928
4066
|
this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
|
|
4067
|
+
this.listenMoreGeometry(true);
|
|
3929
4068
|
}
|
|
3930
4069
|
unbindEvents() {
|
|
3931
4070
|
const r = this.root;
|
|
@@ -3944,6 +4083,7 @@ var Calendar = class {
|
|
|
3944
4083
|
r.removeEventListener("pointerover", this.onAdoptCheck);
|
|
3945
4084
|
r.removeEventListener("focusin", this.onAdoptCheck);
|
|
3946
4085
|
this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
|
|
4086
|
+
this.listenMoreGeometry(false);
|
|
3947
4087
|
this.listenDoc(false);
|
|
3948
4088
|
}
|
|
3949
4089
|
listenDoc(on) {
|
|
@@ -3989,6 +4129,7 @@ var Calendar = class {
|
|
|
3989
4129
|
mode = "select";
|
|
3990
4130
|
}
|
|
3991
4131
|
const touch = ev.pointerType === "touch";
|
|
4132
|
+
const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
|
|
3992
4133
|
const s = {
|
|
3993
4134
|
mode,
|
|
3994
4135
|
pointerId: ev.pointerId,
|
|
@@ -3997,7 +4138,7 @@ var Calendar = class {
|
|
|
3997
4138
|
y0: ev.clientY,
|
|
3998
4139
|
active: false,
|
|
3999
4140
|
origin,
|
|
4000
|
-
grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
|
|
4141
|
+
grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
|
|
4001
4142
|
proposal: null,
|
|
4002
4143
|
denied: false,
|
|
4003
4144
|
timer: null,
|
|
@@ -4500,6 +4641,25 @@ var Calendar = class {
|
|
|
4500
4641
|
if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
|
|
4501
4642
|
}
|
|
4502
4643
|
onKeyDown = (ev) => {
|
|
4644
|
+
if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
|
|
4645
|
+
const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
|
|
4646
|
+
const index = rows.indexOf(this.doc.activeElement);
|
|
4647
|
+
const next = ev.key === "Home" ? 0 : ev.key === "End" ? rows.length - 1 : ev.key === "ArrowDown" ? (index + 1) % rows.length : index <= 0 ? rows.length - 1 : index - 1;
|
|
4648
|
+
ev.preventDefault();
|
|
4649
|
+
ev.stopPropagation();
|
|
4650
|
+
rows[next]?.focus();
|
|
4651
|
+
rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
4652
|
+
return;
|
|
4653
|
+
}
|
|
4654
|
+
if (ev.key === "Tab" && this.popover) {
|
|
4655
|
+
const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
|
|
4656
|
+
const index = controls.indexOf(this.doc.activeElement);
|
|
4657
|
+
const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
|
|
4658
|
+
ev.preventDefault();
|
|
4659
|
+
ev.stopPropagation();
|
|
4660
|
+
controls[next]?.focus();
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4503
4663
|
if (ev.key === "Escape" && this.popover) {
|
|
4504
4664
|
ev.preventDefault();
|
|
4505
4665
|
ev.stopPropagation();
|