@tasknotes/calendar 0.1.3 → 0.1.5

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.
@@ -949,6 +949,9 @@ function defaultOptions() {
949
949
  listDayCount: 7,
950
950
  listStart: "date",
951
951
  listAgenda: false,
952
+ dateStrip: false,
953
+ listLeadTitle: null,
954
+ listAddText: null,
952
955
  monthDisplay: "full",
953
956
  timeZone: "local",
954
957
  locale: void 0,
@@ -999,6 +1002,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
999
1002
  "listDayCount",
1000
1003
  "listStart",
1001
1004
  "listAgenda",
1005
+ "dateStrip",
1006
+ "listLeadTitle",
1007
+ "listAddText",
1002
1008
  "monthDisplay",
1003
1009
  "timeZone",
1004
1010
  "locale",
@@ -1204,17 +1210,22 @@ var DayGridView = class {
1204
1210
  const n = w.days.length;
1205
1211
  const hidden = new Array(n).fill([]).map(() => []);
1206
1212
  w.pool.begin();
1213
+ const previewLevels = new Array(n).fill(0);
1207
1214
  let maxLevel = -1;
1208
1215
  for (const p of row.placements) {
1209
1216
  let level = p.level;
1210
1217
  let room = Infinity;
1211
1218
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visible(c));
1212
1219
  if (level >= room) {
1213
- if (p.item.id !== ctx.draggingId) {
1220
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
1214
1221
  for (let c = p.startCol; c < p.endCol; c++) hidden[c].push(p.item);
1215
1222
  continue;
1216
1223
  }
1217
- level = Math.max(0, room - 1);
1224
+ if (p.item.item.preview) {
1225
+ level = 0;
1226
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visible(c) + 1, previewLevels[c]);
1227
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
1228
+ } else level = Math.max(0, room - 1);
1218
1229
  }
1219
1230
  maxLevel = Math.max(maxLevel, level);
1220
1231
  const single = !p.item.allDay && p.item.endDay - p.item.startDay <= 1;
@@ -1340,12 +1351,244 @@ var DayGridView = class {
1340
1351
  }
1341
1352
  };
1342
1353
 
1354
+ // src/dom/views/list.ts
1355
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1356
+ var ListView = class {
1357
+ constructor(ctx) {
1358
+ this.ctx = ctx;
1359
+ const { doc, win, opts } = ctx;
1360
+ this.el = h(doc, "div", "tnc-list");
1361
+ this.scroller = h(doc, "div", "tnc-list-scroll");
1362
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1363
+ this.scroller.append(this.body);
1364
+ this.el.append(this.scroller);
1365
+ this.agenda = opts.listAgenda;
1366
+ if (!this.agenda) return;
1367
+ this.el.classList.add("tnc-list--agenda");
1368
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1369
+ this.scroller.append(this.sentinel);
1370
+ const IO = win.IntersectionObserver;
1371
+ if (IO) {
1372
+ this.loader = new IO(
1373
+ (entries) => {
1374
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1375
+ },
1376
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1377
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1378
+ );
1379
+ }
1380
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1381
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
1382
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1383
+ }
1384
+ ctx;
1385
+ el;
1386
+ scroller;
1387
+ body;
1388
+ agenda;
1389
+ pools = /* @__PURE__ */ new Map();
1390
+ sections = /* @__PURE__ */ new Map();
1391
+ lead = null;
1392
+ sentinel = null;
1393
+ loader = null;
1394
+ frame = null;
1395
+ /** A picked day stays marked until the list is scrolled by hand. */
1396
+ pinned = false;
1397
+ itemsOn(day) {
1398
+ const { store, opts } = this.ctx;
1399
+ const out = store.spanningIn(day, day + 1, true);
1400
+ const seen = new Set(out.map((n) => n.id));
1401
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
1402
+ return out.filter((n) => n.item.display !== "background" && n.item.display !== "none" && (opts.allDaySlot || !n.allDay)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.ctx.order(a, b));
1403
+ }
1404
+ render(_change) {
1405
+ const { ctx, agenda } = this;
1406
+ const { doc, days, opts } = ctx;
1407
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
1408
+ const leadItems = [];
1409
+ const inLead = /* @__PURE__ */ new Set();
1410
+ const keep = /* @__PURE__ */ new Set();
1411
+ let prev = null;
1412
+ for (const day of days) {
1413
+ let items = this.itemsOn(day);
1414
+ if (includes) {
1415
+ items = items.filter((n) => {
1416
+ if (!includes(n.item)) return true;
1417
+ if (!inLead.has(n.id)) {
1418
+ inLead.add(n.id);
1419
+ leadItems.push(n);
1420
+ }
1421
+ return false;
1422
+ });
1423
+ }
1424
+ if (!items.length && !agenda) continue;
1425
+ keep.add(day);
1426
+ let section = this.sections.get(day);
1427
+ let pool = this.pools.get(day);
1428
+ if (!section || !pool) {
1429
+ const arg = ctx.dayArg(day);
1430
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1431
+ const custom = opts.dayHeaderContent?.(arg);
1432
+ const label = custom ?? ctx.fmt.longDate(day);
1433
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1434
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1435
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
1436
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
1437
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
1438
+ pool = new ItemPool(ctx, rows);
1439
+ this.sections.set(day, section);
1440
+ this.pools.set(day, pool);
1441
+ }
1442
+ section.classList.toggle("is-empty", !items.length);
1443
+ this.fill(pool, items, () => day);
1444
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1445
+ if (prev) prev.after(section);
1446
+ else this.body.prepend(section);
1447
+ }
1448
+ prev = section;
1449
+ }
1450
+ for (const [day, section] of this.sections) {
1451
+ if (keep.has(day)) continue;
1452
+ this.pools.get(day)?.clear();
1453
+ this.pools.delete(day);
1454
+ section.remove();
1455
+ this.sections.delete(day);
1456
+ }
1457
+ this.renderLead(leadItems);
1458
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
1459
+ if (!keep.size && !leadItems.length) {
1460
+ if (!empty) {
1461
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1462
+ this.body.append(empty);
1463
+ }
1464
+ } else empty?.remove();
1465
+ this.ctx.strip?.draw();
1466
+ if (this.loader && this.sentinel) {
1467
+ this.loader.unobserve(this.sentinel);
1468
+ this.loader.observe(this.sentinel);
1469
+ }
1470
+ }
1471
+ /** Places items in a pool, keeping DOM order equal to item order. */
1472
+ fill(pool, items, dayFor) {
1473
+ pool.begin();
1474
+ for (const n of items) {
1475
+ const d = dayFor(n);
1476
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
1477
+ }
1478
+ pool.end();
1479
+ for (const n of items) {
1480
+ const el = pool.elementsFor(n.id)[0];
1481
+ if (el) pool.container.append(el);
1482
+ }
1483
+ }
1484
+ renderLead(items) {
1485
+ const { ctx } = this;
1486
+ const { doc, opts } = ctx;
1487
+ if (!items.length || opts.listLeadTitle === null) {
1488
+ if (!this.lead) return;
1489
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
1490
+ this.lead.pool.clear();
1491
+ this.lead.section.remove();
1492
+ this.lead = null;
1493
+ return;
1494
+ }
1495
+ if (!this.lead) {
1496
+ const title = opts.listLeadTitle;
1497
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
1498
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
1499
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1500
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
1501
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
1502
+ opts.listLeadDidMount?.({ el: slot });
1503
+ }
1504
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
1505
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
1506
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
1507
+ }
1508
+ addRow(day) {
1509
+ const { ctx } = this;
1510
+ const label = ctx.opts.listAddText ?? "";
1511
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
1512
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
1513
+ return btn;
1514
+ }
1515
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
1516
+ scrollToDay(day) {
1517
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1518
+ if (!section) return;
1519
+ this.ctx.strip?.measure();
1520
+ section.scrollIntoView({ block: "start" });
1521
+ this.ctx.strip?.setFocus(day);
1522
+ this.pinned = true;
1523
+ }
1524
+ fold = () => {
1525
+ this.pinned = false;
1526
+ const strip = this.ctx.strip;
1527
+ if (strip?.expanded) strip.setExpanded(false);
1528
+ };
1529
+ onScroll = () => {
1530
+ if (this.frame !== null) return;
1531
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
1532
+ this.frame = null;
1533
+ this.followTop();
1534
+ });
1535
+ };
1536
+ /** Marks the first day not hidden under the strip. */
1537
+ followTop() {
1538
+ const strip = this.ctx.strip;
1539
+ if (!strip || this.pinned) return;
1540
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
1541
+ for (const day of this.ctx.days) {
1542
+ const section = this.sections.get(day);
1543
+ if (section && section.getBoundingClientRect().bottom > edge) {
1544
+ strip.setFocus(day);
1545
+ return;
1546
+ }
1547
+ }
1548
+ }
1549
+ showSelection(_sel) {
1550
+ }
1551
+ hit(x, y) {
1552
+ const el = this.ctx.doc.elementFromPoint(x, y);
1553
+ const section = el?.closest(".tnc-list-section");
1554
+ if (!section || !this.body.contains(section)) return null;
1555
+ if (section === this.lead?.section) {
1556
+ const item = el?.closest(".tnc-item");
1557
+ const meta = item ? this.ctx.meta.get(item) : void 0;
1558
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
1559
+ }
1560
+ const day = parseDay(section.dataset.date ?? "");
1561
+ return day === null ? null : { kind: "day", day };
1562
+ }
1563
+ getScroll() {
1564
+ return { top: this.scroller.scrollTop, left: 0 };
1565
+ }
1566
+ setScroll(s) {
1567
+ this.scroller.scrollTop = s.top;
1568
+ }
1569
+ elementsFor(id) {
1570
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1571
+ return pools.flatMap((p) => p.elementsFor(id));
1572
+ }
1573
+ destroy() {
1574
+ const { win, opts } = this.ctx;
1575
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
1576
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1577
+ this.loader?.disconnect();
1578
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
1579
+ this.lead?.pool.clear();
1580
+ for (const p of this.pools.values()) p.clear();
1581
+ this.el.remove();
1582
+ }
1583
+ };
1584
+
1343
1585
  // src/dom/views/strip.ts
1344
1586
  var SWIPE_PX = 40;
1345
1587
  var DateStrip = class {
1346
- constructor(ctx, pick, focus) {
1588
+ constructor(ctx, pick, focus, onShown) {
1347
1589
  this.ctx = ctx;
1348
1590
  this.pick = pick;
1591
+ this.onShown = onShown;
1349
1592
  const { doc, text } = ctx;
1350
1593
  this.focus = focus;
1351
1594
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1357,8 +1600,10 @@ var DateStrip = class {
1357
1600
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1358
1601
  prev.addEventListener("click", () => this.step(-1));
1359
1602
  next.addEventListener("click", () => this.step(1));
1603
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
1604
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
1360
1605
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1361
- head.append(this.title, prev, next, this.slot);
1606
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1362
1607
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1363
1608
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1364
1609
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -1375,27 +1620,45 @@ var DateStrip = class {
1375
1620
  }
1376
1621
  ctx;
1377
1622
  pick;
1623
+ onShown;
1378
1624
  el;
1379
1625
  title;
1380
1626
  dows;
1381
1627
  grid;
1382
1628
  slot;
1629
+ todayBtn;
1383
1630
  /** Day at the top of the list. */
1384
1631
  focus;
1385
1632
  /** First day of the week shown, or the month shown when expanded. */
1386
1633
  shown;
1387
1634
  target = null;
1635
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1636
+ range = null;
1637
+ shownKey = "";
1388
1638
  swipe = null;
1389
1639
  swallowClick = false;
1390
1640
  expanded = false;
1391
- /** The list scrolled: mark its top day and keep it in view. */
1392
- setFocus(day) {
1393
- if (day === this.focus) return;
1394
- this.focus = day;
1395
- if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1396
- else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
1641
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
1642
+ setFocus(day, range = this.range) {
1643
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
1644
+ if (day === this.focus && sameRange) return;
1645
+ this.range = range;
1646
+ if (day !== this.focus) {
1647
+ this.focus = day;
1648
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1649
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
1650
+ }
1397
1651
  this.draw();
1398
1652
  }
1653
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
1654
+ measure() {
1655
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
1656
+ }
1657
+ /** First day and end (exclusive) of the days on show. */
1658
+ window() {
1659
+ const days = this.days();
1660
+ return { start: days[0], end: days[days.length - 1] + 1 };
1661
+ }
1399
1662
  setExpanded(on) {
1400
1663
  if (on === this.expanded) return;
1401
1664
  this.expanded = on;
@@ -1433,6 +1696,7 @@ var DateStrip = class {
1433
1696
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1434
1697
  this.title.setAttribute("aria-expanded", String(this.expanded));
1435
1698
  this.el.classList.toggle("is-expanded", this.expanded);
1699
+ this.todayBtn.hidden = this.focus === ctx.today;
1436
1700
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1437
1701
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1438
1702
  const month = ymdOfDay(this.shown).month;
@@ -1449,6 +1713,7 @@ var DateStrip = class {
1449
1713
  "tnc-strip-day",
1450
1714
  ...ctx.dayClasses(d),
1451
1715
  d === this.focus ? "is-focus" : "",
1716
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1452
1717
  count ? "has-items" : "",
1453
1718
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1454
1719
  d === this.target ? "is-drop-target" : ""
@@ -1468,6 +1733,11 @@ var DateStrip = class {
1468
1733
  this.grid.replaceChildren(...cells);
1469
1734
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1470
1735
  if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1736
+ const key = `${days[0]}:${days.length}`;
1737
+ if (key !== this.shownKey) {
1738
+ this.shownKey = key;
1739
+ this.onShown();
1740
+ }
1471
1741
  }
1472
1742
  onClick = (ev) => {
1473
1743
  const day = this.dayAt(ev.target);
@@ -1522,205 +1792,6 @@ var DateStrip = class {
1522
1792
  }
1523
1793
  };
1524
1794
 
1525
- // src/dom/views/list.ts
1526
- var LOAD_AHEAD = "0px 0px 600px 0px";
1527
- var ListView = class {
1528
- constructor(ctx) {
1529
- this.ctx = ctx;
1530
- const { doc, win, opts } = ctx;
1531
- this.el = h(doc, "div", "tnc-list");
1532
- this.scroller = h(doc, "div", "tnc-list-scroll");
1533
- this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1534
- this.scroller.append(this.body);
1535
- this.el.append(this.scroller);
1536
- if (!opts.listAgenda) return;
1537
- this.el.classList.add("tnc-list--agenda");
1538
- this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
1539
- this.el.prepend(this.strip.el);
1540
- this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1541
- this.scroller.append(this.sentinel);
1542
- const globals = win;
1543
- const IO = globals.IntersectionObserver;
1544
- if (IO) {
1545
- this.loader = new IO(
1546
- (entries) => {
1547
- if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1548
- },
1549
- // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1550
- { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1551
- );
1552
- }
1553
- const RO = globals.ResizeObserver;
1554
- if (RO) {
1555
- const strip = this.strip.el;
1556
- this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
1557
- this.stripSize.observe(strip);
1558
- }
1559
- win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1560
- this.scroller.addEventListener("wheel", this.fold, { passive: true });
1561
- this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1562
- }
1563
- ctx;
1564
- el;
1565
- scroller;
1566
- body;
1567
- pools = /* @__PURE__ */ new Map();
1568
- sections = /* @__PURE__ */ new Map();
1569
- strip = null;
1570
- sentinel = null;
1571
- loader = null;
1572
- stripSize = null;
1573
- frame = null;
1574
- /** A picked day stays marked until the list is scrolled by hand. */
1575
- pinned = false;
1576
- itemsOn(day) {
1577
- const { store, opts } = this.ctx;
1578
- const out = store.spanningIn(day, day + 1, true);
1579
- const seen = new Set(out.map((n) => n.id));
1580
- for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
1581
- return out.filter((n) => n.item.display !== "background" && n.item.display !== "none" && (opts.allDaySlot || !n.allDay)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.ctx.order(a, b));
1582
- }
1583
- render(_change) {
1584
- const { ctx } = this;
1585
- const { doc, days } = ctx;
1586
- const agenda = !!this.strip;
1587
- const keep = /* @__PURE__ */ new Set();
1588
- let prev = null;
1589
- for (const day of days) {
1590
- const items = this.itemsOn(day);
1591
- if (!items.length && !agenda) continue;
1592
- keep.add(day);
1593
- let section = this.sections.get(day);
1594
- let pool = this.pools.get(day);
1595
- if (!section || !pool) {
1596
- const arg = ctx.dayArg(day);
1597
- const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1598
- const custom = ctx.opts.dayHeaderContent?.(arg);
1599
- const label = custom ?? ctx.fmt.longDate(day);
1600
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1601
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
1602
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
1603
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
1604
- pool = new ItemPool(ctx, rows2);
1605
- this.sections.set(day, section);
1606
- this.pools.set(day, pool);
1607
- }
1608
- section.classList.toggle("is-empty", !items.length);
1609
- pool.begin();
1610
- for (const n of items) {
1611
- pool.place(n.id, {
1612
- n,
1613
- day,
1614
- isStart: n.startDay === day,
1615
- isEnd: n.endDay === day + 1,
1616
- variant: "list",
1617
- mirror: false
1618
- });
1619
- }
1620
- pool.end();
1621
- const rows = pool.container;
1622
- for (const n of items) {
1623
- const el = pool.elementsFor(n.id)[0];
1624
- if (el) rows.append(el);
1625
- }
1626
- if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1627
- if (prev) prev.after(section);
1628
- else this.body.prepend(section);
1629
- }
1630
- prev = section;
1631
- }
1632
- for (const [day, section] of this.sections) {
1633
- if (keep.has(day)) continue;
1634
- this.pools.get(day)?.clear();
1635
- this.pools.delete(day);
1636
- section.remove();
1637
- this.sections.delete(day);
1638
- }
1639
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1640
- if (!keep.size) {
1641
- if (!empty) {
1642
- empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1643
- this.body.append(empty);
1644
- }
1645
- } else empty?.remove();
1646
- this.strip?.draw();
1647
- if (this.loader && this.sentinel) {
1648
- this.loader.unobserve(this.sentinel);
1649
- this.loader.observe(this.sentinel);
1650
- }
1651
- }
1652
- /** A strip day: the host hears of it, and the list brings it to the top. */
1653
- pick(day, ev) {
1654
- this.ctx.dateClick(day, ev);
1655
- this.ctx.showListDay(day);
1656
- }
1657
- /** Brings a day's section to the top, under the strip. */
1658
- scrollToDay(day) {
1659
- const section = this.sections.get(day);
1660
- if (!section) return;
1661
- if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
1662
- section.scrollIntoView({ block: "start" });
1663
- this.strip?.setFocus(day);
1664
- this.pinned = true;
1665
- }
1666
- fold = () => {
1667
- this.pinned = false;
1668
- if (this.strip?.expanded) this.strip.setExpanded(false);
1669
- };
1670
- onScroll = () => {
1671
- if (this.frame !== null) return;
1672
- this.frame = this.ctx.win.requestAnimationFrame(() => {
1673
- this.frame = null;
1674
- this.followTop();
1675
- });
1676
- };
1677
- /** Marks the first day not hidden under the strip. */
1678
- followTop() {
1679
- if (!this.strip || this.pinned) return;
1680
- const edge = this.strip.el.getBoundingClientRect().bottom + 1;
1681
- for (const day of this.ctx.days) {
1682
- const section = this.sections.get(day);
1683
- if (section && section.getBoundingClientRect().bottom > edge) {
1684
- this.strip.setFocus(day);
1685
- return;
1686
- }
1687
- }
1688
- }
1689
- showSelection(_sel) {
1690
- }
1691
- hit(x, y) {
1692
- const el = this.ctx.doc.elementFromPoint(x, y);
1693
- const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
1694
- this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
1695
- if (stripDay !== null) return { kind: "day", day: stripDay };
1696
- const section = el?.closest(".tnc-list-section");
1697
- const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
1698
- return day === null ? null : { kind: "day", day };
1699
- }
1700
- dragEnded() {
1701
- this.strip?.markTarget(null);
1702
- }
1703
- getScroll() {
1704
- return { top: this.scroller.scrollTop, left: 0 };
1705
- }
1706
- setScroll(s) {
1707
- this.scroller.scrollTop = s.top;
1708
- }
1709
- elementsFor(id) {
1710
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1711
- }
1712
- destroy() {
1713
- const { win } = this.ctx;
1714
- win.removeEventListener("scroll", this.onScroll, { capture: true });
1715
- if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1716
- this.loader?.disconnect();
1717
- this.stripSize?.disconnect();
1718
- this.strip?.destroy();
1719
- for (const p of this.pools.values()) p.clear();
1720
- this.el.remove();
1721
- }
1722
- };
1723
-
1724
1795
  // src/dom/views/timegrid.ts
1725
1796
  var BAR_GAP2 = 2;
1726
1797
  var TimeGridView = class {
@@ -1939,17 +2010,22 @@ var TimeGridView = class {
1939
2010
  const pool = this.allDayPool;
1940
2011
  pool.begin();
1941
2012
  const hidden = new Array(days.length).fill(0);
2013
+ const previewLevels = new Array(days.length).fill(0);
1942
2014
  let maxLevel = -1;
1943
2015
  for (const p of row.placements) {
1944
2016
  let level = p.level;
1945
2017
  let room = Infinity;
1946
2018
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visibleLevels(c));
1947
2019
  if (level >= room) {
1948
- if (p.item.id !== ctx.draggingId) {
2020
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
1949
2021
  for (let c = p.startCol; c < p.endCol; c++) hidden[c]++;
1950
2022
  continue;
1951
2023
  }
1952
- level = Math.max(0, room - 1);
2024
+ if (p.item.item.preview) {
2025
+ level = 0;
2026
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visibleLevels(c) + 1, previewLevels[c]);
2027
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
2028
+ } else level = Math.max(0, room - 1);
1953
2029
  }
1954
2030
  maxLevel = Math.max(maxLevel, level);
1955
2031
  const el = pool.place(p.item.id, {
@@ -2037,7 +2113,8 @@ var TimeGridView = class {
2037
2113
  const hiddenByCluster = /* @__PURE__ */ new Map();
2038
2114
  pool.begin();
2039
2115
  for (const p of placements) {
2040
- if (withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2116
+ const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2117
+ if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2041
2118
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
2042
2119
  rec.count++;
2043
2120
  rec.start = Math.min(rec.start, p.startMin);
@@ -2046,7 +2123,7 @@ var TimeGridView = class {
2046
2123
  hiddenByCluster.set(p.cluster, rec);
2047
2124
  continue;
2048
2125
  }
2049
- const cols = withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2126
+ const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2050
2127
  const span = Math.min(p.span, cols - p.col);
2051
2128
  const left = p.col / cols;
2052
2129
  let width = span / cols;
@@ -2926,6 +3003,9 @@ var Calendar = class {
2926
3003
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2927
3004
  draggingId = null;
2928
3005
  dragRow = null;
3006
+ strip = null;
3007
+ stripSize = null;
3008
+ rebuilding = false;
2929
3009
  toolbarEl;
2930
3010
  viewHost;
2931
3011
  current = null;
@@ -3139,7 +3219,7 @@ var Calendar = class {
3139
3219
  }
3140
3220
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3141
3221
  hitTest(clientX, clientY) {
3142
- return this.current?.hit(clientX, clientY) ?? null;
3222
+ return this.hitAt(clientX, clientY);
3143
3223
  }
3144
3224
  /** Item elements currently rendered for an id. */
3145
3225
  elementsFor(id) {
@@ -3181,6 +3261,9 @@ var Calendar = class {
3181
3261
  this.unbindEvents();
3182
3262
  this.current?.destroy();
3183
3263
  this.current = null;
3264
+ this.stripSize?.disconnect();
3265
+ this.strip?.destroy();
3266
+ this.strip = null;
3184
3267
  this.root.remove();
3185
3268
  }
3186
3269
  /* ========================================================== view context */
@@ -3211,7 +3294,7 @@ var Calendar = class {
3211
3294
  return out;
3212
3295
  }
3213
3296
  isEditable(n, what) {
3214
- if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
3297
+ if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3215
3298
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3216
3299
  const it = n.item;
3217
3300
  const base = it.editable ?? this.opts.editable;
@@ -3237,6 +3320,7 @@ var Calendar = class {
3237
3320
  createItemEl(meta) {
3238
3321
  const { n, variant } = meta;
3239
3322
  const it = n.item;
3323
+ const preview = meta.mirror || it.preview === true;
3240
3324
  const el = h(this.doc, "div", "tnc-item");
3241
3325
  el.classList.add(`tnc-item--${variant}`);
3242
3326
  if (it.kind) el.classList.add(`tnc-k-${it.kind.replace(/[^\w-]/g, "-")}`);
@@ -3246,12 +3330,12 @@ var Calendar = class {
3246
3330
  if (!meta.isStart) el.classList.add("is-cont-start");
3247
3331
  if (!meta.isEnd) el.classList.add("is-cont-end");
3248
3332
  if (n.allDay) el.classList.add("is-all-day");
3249
- if (meta.mirror || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3333
+ if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3250
3334
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3251
3335
  const indicator = variant === "indicator";
3252
- if (!meta.mirror && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3336
+ if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3253
3337
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3254
- if (this.selectedIds.has(n.id) && !meta.mirror) el.classList.add("is-selected");
3338
+ if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3255
3339
  el.classList.add(...classList(it.className));
3256
3340
  const arg = this.itemArg(el, meta);
3257
3341
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
@@ -3262,7 +3346,7 @@ var Calendar = class {
3262
3346
  if (it.backgroundColor) el.style.setProperty("--tnc-bg", it.backgroundColor);
3263
3347
  if (it.borderColor) el.style.setProperty("--tnc-border", it.borderColor);
3264
3348
  if (it.textColor) el.style.setProperty("--tnc-fg", it.textColor);
3265
- if (indicator) el.setAttribute("aria-hidden", "true");
3349
+ if (indicator || preview) el.setAttribute("aria-hidden", "true");
3266
3350
  else if (it.display !== "background") {
3267
3351
  el.tabIndex = 0;
3268
3352
  el.setAttribute("role", "button");
@@ -3279,7 +3363,7 @@ var Calendar = class {
3279
3363
  } else {
3280
3364
  el.append(this.defaultContent(meta));
3281
3365
  }
3282
- if (variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
3366
+ if (!preview && variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
3283
3367
  this.opts.itemDidMount?.(arg);
3284
3368
  return el;
3285
3369
  }
@@ -3288,7 +3372,7 @@ var Calendar = class {
3288
3372
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3289
3373
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
3290
3374
  el.classList.toggle("is-conflict", this.conflicts.has(meta.n.id));
3291
- el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
3375
+ el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
3292
3376
  }
3293
3377
  selectedSet() {
3294
3378
  const s = this.opts.selected;
@@ -3298,7 +3382,7 @@ var Calendar = class {
3298
3382
  this.selectedIds = this.selectedSet();
3299
3383
  for (const el of this.viewHost.querySelectorAll(".tnc-item")) {
3300
3384
  const meta = this.meta.get(el);
3301
- if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
3385
+ if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
3302
3386
  }
3303
3387
  }
3304
3388
  destroyItemEl(el) {
@@ -3449,12 +3533,18 @@ var Calendar = class {
3449
3533
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3450
3534
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3451
3535
  if (navigated) this.listEnd = null;
3452
- const rangeChanged = this.applyRange();
3536
+ const prev = this.view;
3537
+ this.applyRange();
3453
3538
  this.conflicts = dependencyConflicts(this.store);
3454
3539
  this.root.className = "tnc";
3455
3540
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3456
3541
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3457
3542
  this.renderToolbar();
3543
+ this.rebuilding = true;
3544
+ this.syncStrip(navigated);
3545
+ this.rebuilding = false;
3546
+ this.view.dataRange = this.dataRangeFor(this.view.range);
3547
+ const rangeChanged = rangeMoved(prev, this.view);
3458
3548
  this.current?.destroy();
3459
3549
  this.current = this.createView();
3460
3550
  this.viewHost.replaceChildren(this.current.el);
@@ -3467,7 +3557,7 @@ var Calendar = class {
3467
3557
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3468
3558
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3469
3559
  }
3470
- /** Sets the visible days and view info from the anchor; true when the range moved. */
3560
+ /** Sets the visible days and view info from the anchor. */
3471
3561
  applyRange() {
3472
3562
  const { startDay, endDay } = this.computeRange();
3473
3563
  const hidden = new Set(this.opts.hiddenDays);
@@ -3485,10 +3575,74 @@ var Calendar = class {
3485
3575
  start: new Date(toInstant(startDay, 0, this.zone)),
3486
3576
  end: new Date(toInstant(endDay, 0, this.zone))
3487
3577
  };
3488
- const prevView = this.view;
3489
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
3578
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
3490
3579
  this.view.title = this.computeTitle(startDay, endDay);
3491
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
3580
+ }
3581
+ /** The range, run on to the end of the strip's days so their dots are right. */
3582
+ dataRangeFor(range) {
3583
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3584
+ if (end === range.endDay) return range;
3585
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3586
+ }
3587
+ /** Adds, updates or removes the date strip for the current view. */
3588
+ syncStrip(navigated) {
3589
+ const { view, listAgenda, dateStrip } = this.opts;
3590
+ const list = view === "list";
3591
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
3592
+ if (!want) {
3593
+ if (!this.strip) return;
3594
+ this.stripSize?.disconnect();
3595
+ this.stripSize = null;
3596
+ this.strip.destroy();
3597
+ this.strip = null;
3598
+ this.root.style.removeProperty("--tnc-strip-h");
3599
+ return;
3600
+ }
3601
+ this.root.classList.add("tnc--has-strip");
3602
+ const { startDay, endDay } = this.view.range;
3603
+ if (!this.strip) {
3604
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3605
+ this.strip = strip;
3606
+ this.root.insertBefore(strip.el, this.viewHost);
3607
+ const RO = this.win.ResizeObserver;
3608
+ if (RO) {
3609
+ this.stripSize = new RO(() => strip.measure());
3610
+ this.stripSize.observe(strip.el);
3611
+ }
3612
+ }
3613
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
3614
+ else if (navigated) this.strip.setFocus(startDay, null);
3615
+ this.strip.draw();
3616
+ }
3617
+ pickStripDay(day, ev) {
3618
+ this.dateClick(day, ev);
3619
+ if (this.agendaList()) {
3620
+ this.showListDay(day);
3621
+ return;
3622
+ }
3623
+ const { startDay, endDay } = this.view.range;
3624
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
3625
+ this.anchor = day;
3626
+ this.rebuild(true);
3627
+ }
3628
+ /** The strip moved to other days: load theirs too. */
3629
+ stripShown() {
3630
+ if (!this.strip || this.rebuilding || !this.view) return;
3631
+ const next = this.dataRangeFor(this.view.range);
3632
+ if (next.endDay === this.view.dataRange.endDay) return;
3633
+ this.view = { ...this.view, dataRange: next };
3634
+ this.opts.onRangeChange?.(this.view);
3635
+ }
3636
+ /** What is under a point, the date strip included. */
3637
+ hitAt(x, y) {
3638
+ const strip = this.strip;
3639
+ if (strip) {
3640
+ const el = this.doc.elementFromPoint(x, y);
3641
+ const day = el ? strip.dayAt(el) : null;
3642
+ strip.markTarget(day !== null && this.draggingId ? day : null);
3643
+ if (day !== null) return { kind: "day", day };
3644
+ }
3645
+ return this.current?.hit(x, y) ?? null;
3492
3646
  }
3493
3647
  agendaList() {
3494
3648
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3501,6 +3655,7 @@ var Calendar = class {
3501
3655
  this.listEnd = end;
3502
3656
  this.applyRange();
3503
3657
  this.current?.render(null);
3658
+ this.strip?.draw();
3504
3659
  this.opts.onRangeChange?.(this.view);
3505
3660
  }
3506
3661
  showListDay(day) {
@@ -3622,7 +3777,7 @@ var Calendar = class {
3622
3777
  day: meta.day,
3623
3778
  isStart: meta.isStart,
3624
3779
  isEnd: meta.isEnd,
3625
- isMirror: meta.mirror,
3780
+ isMirror: meta.mirror || meta.n.item.preview === true,
3626
3781
  variant: meta.variant,
3627
3782
  timeText: meta.n.allDay ? "" : end ? `${start} \u2013 ${end}` : start
3628
3783
  };
@@ -3767,7 +3922,7 @@ var Calendar = class {
3767
3922
  }
3768
3923
  itemElFrom(target) {
3769
3924
  const el = target?.closest?.(".tnc-item");
3770
- return el && this.root.contains(el) && this.meta.has(el) ? el : null;
3925
+ return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3771
3926
  }
3772
3927
  onDocPointerDown = (ev) => {
3773
3928
  const t = ev.target;
@@ -3898,7 +4053,7 @@ var Calendar = class {
3898
4053
  };
3899
4054
  /** Proposal for the current pointer position. */
3900
4055
  track(s, ev) {
3901
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
4056
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3902
4057
  if (s.mode === "external") {
3903
4058
  this.trackExternal(s, hit);
3904
4059
  return;
@@ -4024,7 +4179,7 @@ var Calendar = class {
4024
4179
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
4025
4180
  }
4026
4181
  if (hit.kind === "day") {
4027
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4182
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
4028
4183
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
4029
4184
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
4030
4185
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -4159,7 +4314,7 @@ var Calendar = class {
4159
4314
  this.draggingId = null;
4160
4315
  this.dragRow = null;
4161
4316
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4162
- this.current?.dragEnded?.();
4317
+ this.strip?.markTarget(null);
4163
4318
  if (!s.active) return;
4164
4319
  if (s.mode === "select") {
4165
4320
  if (cancel) this.unselectSilently();
@@ -4456,6 +4611,9 @@ function scrollerOf(el, axis) {
4456
4611
  }
4457
4612
  return doc.scrollingElement;
4458
4613
  }
4614
+ function rangeMoved(prev, next) {
4615
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4616
+ }
4459
4617
 
4460
4618
  // src/tasknotes/bases.ts
4461
4619
  var FROM_BASES = {