@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.
@@ -165,6 +165,9 @@ function defaultOptions() {
165
165
  listDayCount: 7,
166
166
  listStart: "date",
167
167
  listAgenda: false,
168
+ dateStrip: false,
169
+ listLeadTitle: null,
170
+ listAddText: null,
168
171
  monthDisplay: "full",
169
172
  timeZone: "local",
170
173
  locale: void 0,
@@ -215,6 +218,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
215
218
  "listDayCount",
216
219
  "listStart",
217
220
  "listAgenda",
221
+ "dateStrip",
222
+ "listLeadTitle",
223
+ "listAddText",
218
224
  "monthDisplay",
219
225
  "timeZone",
220
226
  "locale",
@@ -506,17 +512,22 @@ var DayGridView = class {
506
512
  const n = w.days.length;
507
513
  const hidden = new Array(n).fill([]).map(() => []);
508
514
  w.pool.begin();
515
+ const previewLevels = new Array(n).fill(0);
509
516
  let maxLevel = -1;
510
517
  for (const p of row.placements) {
511
518
  let level = p.level;
512
519
  let room = Infinity;
513
520
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visible(c));
514
521
  if (level >= room) {
515
- if (p.item.id !== ctx.draggingId) {
522
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
516
523
  for (let c = p.startCol; c < p.endCol; c++) hidden[c].push(p.item);
517
524
  continue;
518
525
  }
519
- level = Math.max(0, room - 1);
526
+ if (p.item.item.preview) {
527
+ level = 0;
528
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visible(c) + 1, previewLevels[c]);
529
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
530
+ } else level = Math.max(0, room - 1);
520
531
  }
521
532
  maxLevel = Math.max(maxLevel, level);
522
533
  const single = !p.item.allDay && p.item.endDay - p.item.startDay <= 1;
@@ -642,12 +653,244 @@ var DayGridView = class {
642
653
  }
643
654
  };
644
655
 
656
+ // src/dom/views/list.ts
657
+ var LOAD_AHEAD = "0px 0px 600px 0px";
658
+ var ListView = class {
659
+ constructor(ctx) {
660
+ this.ctx = ctx;
661
+ const { doc, win, opts } = ctx;
662
+ this.el = h(doc, "div", "tnc-list");
663
+ this.scroller = h(doc, "div", "tnc-list-scroll");
664
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
665
+ this.scroller.append(this.body);
666
+ this.el.append(this.scroller);
667
+ this.agenda = opts.listAgenda;
668
+ if (!this.agenda) return;
669
+ this.el.classList.add("tnc-list--agenda");
670
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
671
+ this.scroller.append(this.sentinel);
672
+ const IO = win.IntersectionObserver;
673
+ if (IO) {
674
+ this.loader = new IO(
675
+ (entries) => {
676
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
677
+ },
678
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
679
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
680
+ );
681
+ }
682
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
683
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
684
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
685
+ }
686
+ ctx;
687
+ el;
688
+ scroller;
689
+ body;
690
+ agenda;
691
+ pools = /* @__PURE__ */ new Map();
692
+ sections = /* @__PURE__ */ new Map();
693
+ lead = null;
694
+ sentinel = null;
695
+ loader = null;
696
+ frame = null;
697
+ /** A picked day stays marked until the list is scrolled by hand. */
698
+ pinned = false;
699
+ itemsOn(day) {
700
+ const { store, opts } = this.ctx;
701
+ const out = store.spanningIn(day, day + 1, true);
702
+ const seen = new Set(out.map((n) => n.id));
703
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
704
+ 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));
705
+ }
706
+ render(_change) {
707
+ const { ctx, agenda } = this;
708
+ const { doc, days, opts } = ctx;
709
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
710
+ const leadItems = [];
711
+ const inLead = /* @__PURE__ */ new Set();
712
+ const keep = /* @__PURE__ */ new Set();
713
+ let prev = null;
714
+ for (const day of days) {
715
+ let items = this.itemsOn(day);
716
+ if (includes) {
717
+ items = items.filter((n) => {
718
+ if (!includes(n.item)) return true;
719
+ if (!inLead.has(n.id)) {
720
+ inLead.add(n.id);
721
+ leadItems.push(n);
722
+ }
723
+ return false;
724
+ });
725
+ }
726
+ if (!items.length && !agenda) continue;
727
+ keep.add(day);
728
+ let section = this.sections.get(day);
729
+ let pool = this.pools.get(day);
730
+ if (!section || !pool) {
731
+ const arg = ctx.dayArg(day);
732
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
733
+ const custom = opts.dayHeaderContent?.(arg);
734
+ const label = custom ?? ctx.fmt.longDate(day);
735
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
736
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
737
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
738
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
739
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
740
+ pool = new ItemPool(ctx, rows);
741
+ this.sections.set(day, section);
742
+ this.pools.set(day, pool);
743
+ }
744
+ section.classList.toggle("is-empty", !items.length);
745
+ this.fill(pool, items, () => day);
746
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
747
+ if (prev) prev.after(section);
748
+ else this.body.prepend(section);
749
+ }
750
+ prev = section;
751
+ }
752
+ for (const [day, section] of this.sections) {
753
+ if (keep.has(day)) continue;
754
+ this.pools.get(day)?.clear();
755
+ this.pools.delete(day);
756
+ section.remove();
757
+ this.sections.delete(day);
758
+ }
759
+ this.renderLead(leadItems);
760
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
761
+ if (!keep.size && !leadItems.length) {
762
+ if (!empty) {
763
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
764
+ this.body.append(empty);
765
+ }
766
+ } else empty?.remove();
767
+ this.ctx.strip?.draw();
768
+ if (this.loader && this.sentinel) {
769
+ this.loader.unobserve(this.sentinel);
770
+ this.loader.observe(this.sentinel);
771
+ }
772
+ }
773
+ /** Places items in a pool, keeping DOM order equal to item order. */
774
+ fill(pool, items, dayFor) {
775
+ pool.begin();
776
+ for (const n of items) {
777
+ const d = dayFor(n);
778
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
779
+ }
780
+ pool.end();
781
+ for (const n of items) {
782
+ const el = pool.elementsFor(n.id)[0];
783
+ if (el) pool.container.append(el);
784
+ }
785
+ }
786
+ renderLead(items) {
787
+ const { ctx } = this;
788
+ const { doc, opts } = ctx;
789
+ if (!items.length || opts.listLeadTitle === null) {
790
+ if (!this.lead) return;
791
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
792
+ this.lead.pool.clear();
793
+ this.lead.section.remove();
794
+ this.lead = null;
795
+ return;
796
+ }
797
+ if (!this.lead) {
798
+ const title = opts.listLeadTitle;
799
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
800
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
801
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
802
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
803
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
804
+ opts.listLeadDidMount?.({ el: slot });
805
+ }
806
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
807
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
808
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
809
+ }
810
+ addRow(day) {
811
+ const { ctx } = this;
812
+ const label = ctx.opts.listAddText ?? "";
813
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
814
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
815
+ return btn;
816
+ }
817
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
818
+ scrollToDay(day) {
819
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
820
+ if (!section) return;
821
+ this.ctx.strip?.measure();
822
+ section.scrollIntoView({ block: "start" });
823
+ this.ctx.strip?.setFocus(day);
824
+ this.pinned = true;
825
+ }
826
+ fold = () => {
827
+ this.pinned = false;
828
+ const strip = this.ctx.strip;
829
+ if (strip?.expanded) strip.setExpanded(false);
830
+ };
831
+ onScroll = () => {
832
+ if (this.frame !== null) return;
833
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
834
+ this.frame = null;
835
+ this.followTop();
836
+ });
837
+ };
838
+ /** Marks the first day not hidden under the strip. */
839
+ followTop() {
840
+ const strip = this.ctx.strip;
841
+ if (!strip || this.pinned) return;
842
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
843
+ for (const day of this.ctx.days) {
844
+ const section = this.sections.get(day);
845
+ if (section && section.getBoundingClientRect().bottom > edge) {
846
+ strip.setFocus(day);
847
+ return;
848
+ }
849
+ }
850
+ }
851
+ showSelection(_sel) {
852
+ }
853
+ hit(x, y) {
854
+ const el = this.ctx.doc.elementFromPoint(x, y);
855
+ const section = el?.closest(".tnc-list-section");
856
+ if (!section || !this.body.contains(section)) return null;
857
+ if (section === this.lead?.section) {
858
+ const item = el?.closest(".tnc-item");
859
+ const meta = item ? this.ctx.meta.get(item) : void 0;
860
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
861
+ }
862
+ const day = parseDay(section.dataset.date ?? "");
863
+ return day === null ? null : { kind: "day", day };
864
+ }
865
+ getScroll() {
866
+ return { top: this.scroller.scrollTop, left: 0 };
867
+ }
868
+ setScroll(s) {
869
+ this.scroller.scrollTop = s.top;
870
+ }
871
+ elementsFor(id) {
872
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
873
+ return pools.flatMap((p) => p.elementsFor(id));
874
+ }
875
+ destroy() {
876
+ const { win, opts } = this.ctx;
877
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
878
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
879
+ this.loader?.disconnect();
880
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
881
+ this.lead?.pool.clear();
882
+ for (const p of this.pools.values()) p.clear();
883
+ this.el.remove();
884
+ }
885
+ };
886
+
645
887
  // src/dom/views/strip.ts
646
888
  var SWIPE_PX = 40;
647
889
  var DateStrip = class {
648
- constructor(ctx, pick, focus) {
890
+ constructor(ctx, pick, focus, onShown) {
649
891
  this.ctx = ctx;
650
892
  this.pick = pick;
893
+ this.onShown = onShown;
651
894
  const { doc, text } = ctx;
652
895
  this.focus = focus;
653
896
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -659,8 +902,10 @@ var DateStrip = class {
659
902
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
660
903
  prev.addEventListener("click", () => this.step(-1));
661
904
  next.addEventListener("click", () => this.step(1));
905
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
906
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
662
907
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
663
- head.append(this.title, prev, next, this.slot);
908
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
664
909
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
665
910
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
666
911
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -677,27 +922,45 @@ var DateStrip = class {
677
922
  }
678
923
  ctx;
679
924
  pick;
925
+ onShown;
680
926
  el;
681
927
  title;
682
928
  dows;
683
929
  grid;
684
930
  slot;
931
+ todayBtn;
685
932
  /** Day at the top of the list. */
686
933
  focus;
687
934
  /** First day of the week shown, or the month shown when expanded. */
688
935
  shown;
689
936
  target = null;
937
+ /** Days tinted as on screen besides the focus (a time grid's range). */
938
+ range = null;
939
+ shownKey = "";
690
940
  swipe = null;
691
941
  swallowClick = false;
692
942
  expanded = false;
693
- /** The list scrolled: mark its top day and keep it in view. */
694
- setFocus(day) {
695
- if (day === this.focus) return;
696
- this.focus = day;
697
- if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
698
- else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
943
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
944
+ setFocus(day, range = this.range) {
945
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
946
+ if (day === this.focus && sameRange) return;
947
+ this.range = range;
948
+ if (day !== this.focus) {
949
+ this.focus = day;
950
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
951
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
952
+ }
699
953
  this.draw();
700
954
  }
955
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
956
+ measure() {
957
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
958
+ }
959
+ /** First day and end (exclusive) of the days on show. */
960
+ window() {
961
+ const days = this.days();
962
+ return { start: days[0], end: days[days.length - 1] + 1 };
963
+ }
701
964
  setExpanded(on) {
702
965
  if (on === this.expanded) return;
703
966
  this.expanded = on;
@@ -735,6 +998,7 @@ var DateStrip = class {
735
998
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
736
999
  this.title.setAttribute("aria-expanded", String(this.expanded));
737
1000
  this.el.classList.toggle("is-expanded", this.expanded);
1001
+ this.todayBtn.hidden = this.focus === ctx.today;
738
1002
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
739
1003
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
740
1004
  const month = ymdOfDay(this.shown).month;
@@ -751,6 +1015,7 @@ var DateStrip = class {
751
1015
  "tnc-strip-day",
752
1016
  ...ctx.dayClasses(d),
753
1017
  d === this.focus ? "is-focus" : "",
1018
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
754
1019
  count ? "has-items" : "",
755
1020
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
756
1021
  d === this.target ? "is-drop-target" : ""
@@ -770,6 +1035,11 @@ var DateStrip = class {
770
1035
  this.grid.replaceChildren(...cells);
771
1036
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
772
1037
  if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1038
+ const key = `${days[0]}:${days.length}`;
1039
+ if (key !== this.shownKey) {
1040
+ this.shownKey = key;
1041
+ this.onShown();
1042
+ }
773
1043
  }
774
1044
  onClick = (ev) => {
775
1045
  const day = this.dayAt(ev.target);
@@ -824,205 +1094,6 @@ var DateStrip = class {
824
1094
  }
825
1095
  };
826
1096
 
827
- // src/dom/views/list.ts
828
- var LOAD_AHEAD = "0px 0px 600px 0px";
829
- var ListView = class {
830
- constructor(ctx) {
831
- this.ctx = ctx;
832
- const { doc, win, opts } = ctx;
833
- this.el = h(doc, "div", "tnc-list");
834
- this.scroller = h(doc, "div", "tnc-list-scroll");
835
- this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
836
- this.scroller.append(this.body);
837
- this.el.append(this.scroller);
838
- if (!opts.listAgenda) return;
839
- this.el.classList.add("tnc-list--agenda");
840
- this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
841
- this.el.prepend(this.strip.el);
842
- this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
843
- this.scroller.append(this.sentinel);
844
- const globals = win;
845
- const IO = globals.IntersectionObserver;
846
- if (IO) {
847
- this.loader = new IO(
848
- (entries) => {
849
- if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
850
- },
851
- // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
852
- { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
853
- );
854
- }
855
- const RO = globals.ResizeObserver;
856
- if (RO) {
857
- const strip = this.strip.el;
858
- this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
859
- this.stripSize.observe(strip);
860
- }
861
- win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
862
- this.scroller.addEventListener("wheel", this.fold, { passive: true });
863
- this.scroller.addEventListener("touchmove", this.fold, { passive: true });
864
- }
865
- ctx;
866
- el;
867
- scroller;
868
- body;
869
- pools = /* @__PURE__ */ new Map();
870
- sections = /* @__PURE__ */ new Map();
871
- strip = null;
872
- sentinel = null;
873
- loader = null;
874
- stripSize = null;
875
- frame = null;
876
- /** A picked day stays marked until the list is scrolled by hand. */
877
- pinned = false;
878
- itemsOn(day) {
879
- const { store, opts } = this.ctx;
880
- const out = store.spanningIn(day, day + 1, true);
881
- const seen = new Set(out.map((n) => n.id));
882
- for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
883
- 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));
884
- }
885
- render(_change) {
886
- const { ctx } = this;
887
- const { doc, days } = ctx;
888
- const agenda = !!this.strip;
889
- const keep = /* @__PURE__ */ new Set();
890
- let prev = null;
891
- for (const day of days) {
892
- const items = this.itemsOn(day);
893
- if (!items.length && !agenda) continue;
894
- keep.add(day);
895
- let section = this.sections.get(day);
896
- let pool = this.pools.get(day);
897
- if (!section || !pool) {
898
- const arg = ctx.dayArg(day);
899
- const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
900
- const custom = ctx.opts.dayHeaderContent?.(arg);
901
- const label = custom ?? ctx.fmt.longDate(day);
902
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
903
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
904
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
905
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
906
- pool = new ItemPool(ctx, rows2);
907
- this.sections.set(day, section);
908
- this.pools.set(day, pool);
909
- }
910
- section.classList.toggle("is-empty", !items.length);
911
- pool.begin();
912
- for (const n of items) {
913
- pool.place(n.id, {
914
- n,
915
- day,
916
- isStart: n.startDay === day,
917
- isEnd: n.endDay === day + 1,
918
- variant: "list",
919
- mirror: false
920
- });
921
- }
922
- pool.end();
923
- const rows = pool.container;
924
- for (const n of items) {
925
- const el = pool.elementsFor(n.id)[0];
926
- if (el) rows.append(el);
927
- }
928
- if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
929
- if (prev) prev.after(section);
930
- else this.body.prepend(section);
931
- }
932
- prev = section;
933
- }
934
- for (const [day, section] of this.sections) {
935
- if (keep.has(day)) continue;
936
- this.pools.get(day)?.clear();
937
- this.pools.delete(day);
938
- section.remove();
939
- this.sections.delete(day);
940
- }
941
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
942
- if (!keep.size) {
943
- if (!empty) {
944
- empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
945
- this.body.append(empty);
946
- }
947
- } else empty?.remove();
948
- this.strip?.draw();
949
- if (this.loader && this.sentinel) {
950
- this.loader.unobserve(this.sentinel);
951
- this.loader.observe(this.sentinel);
952
- }
953
- }
954
- /** A strip day: the host hears of it, and the list brings it to the top. */
955
- pick(day, ev) {
956
- this.ctx.dateClick(day, ev);
957
- this.ctx.showListDay(day);
958
- }
959
- /** Brings a day's section to the top, under the strip. */
960
- scrollToDay(day) {
961
- const section = this.sections.get(day);
962
- if (!section) return;
963
- if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
964
- section.scrollIntoView({ block: "start" });
965
- this.strip?.setFocus(day);
966
- this.pinned = true;
967
- }
968
- fold = () => {
969
- this.pinned = false;
970
- if (this.strip?.expanded) this.strip.setExpanded(false);
971
- };
972
- onScroll = () => {
973
- if (this.frame !== null) return;
974
- this.frame = this.ctx.win.requestAnimationFrame(() => {
975
- this.frame = null;
976
- this.followTop();
977
- });
978
- };
979
- /** Marks the first day not hidden under the strip. */
980
- followTop() {
981
- if (!this.strip || this.pinned) return;
982
- const edge = this.strip.el.getBoundingClientRect().bottom + 1;
983
- for (const day of this.ctx.days) {
984
- const section = this.sections.get(day);
985
- if (section && section.getBoundingClientRect().bottom > edge) {
986
- this.strip.setFocus(day);
987
- return;
988
- }
989
- }
990
- }
991
- showSelection(_sel) {
992
- }
993
- hit(x, y) {
994
- const el = this.ctx.doc.elementFromPoint(x, y);
995
- const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
996
- this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
997
- if (stripDay !== null) return { kind: "day", day: stripDay };
998
- const section = el?.closest(".tnc-list-section");
999
- const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
1000
- return day === null ? null : { kind: "day", day };
1001
- }
1002
- dragEnded() {
1003
- this.strip?.markTarget(null);
1004
- }
1005
- getScroll() {
1006
- return { top: this.scroller.scrollTop, left: 0 };
1007
- }
1008
- setScroll(s) {
1009
- this.scroller.scrollTop = s.top;
1010
- }
1011
- elementsFor(id) {
1012
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1013
- }
1014
- destroy() {
1015
- const { win } = this.ctx;
1016
- win.removeEventListener("scroll", this.onScroll, { capture: true });
1017
- if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1018
- this.loader?.disconnect();
1019
- this.stripSize?.disconnect();
1020
- this.strip?.destroy();
1021
- for (const p of this.pools.values()) p.clear();
1022
- this.el.remove();
1023
- }
1024
- };
1025
-
1026
1097
  // src/dom/views/timegrid.ts
1027
1098
  var BAR_GAP2 = 2;
1028
1099
  var TimeGridView = class {
@@ -1241,17 +1312,22 @@ var TimeGridView = class {
1241
1312
  const pool = this.allDayPool;
1242
1313
  pool.begin();
1243
1314
  const hidden = new Array(days.length).fill(0);
1315
+ const previewLevels = new Array(days.length).fill(0);
1244
1316
  let maxLevel = -1;
1245
1317
  for (const p of row.placements) {
1246
1318
  let level = p.level;
1247
1319
  let room = Infinity;
1248
1320
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visibleLevels(c));
1249
1321
  if (level >= room) {
1250
- if (p.item.id !== ctx.draggingId) {
1322
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
1251
1323
  for (let c = p.startCol; c < p.endCol; c++) hidden[c]++;
1252
1324
  continue;
1253
1325
  }
1254
- level = Math.max(0, room - 1);
1326
+ if (p.item.item.preview) {
1327
+ level = 0;
1328
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visibleLevels(c) + 1, previewLevels[c]);
1329
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
1330
+ } else level = Math.max(0, room - 1);
1255
1331
  }
1256
1332
  maxLevel = Math.max(maxLevel, level);
1257
1333
  const el = pool.place(p.item.id, {
@@ -1339,7 +1415,8 @@ var TimeGridView = class {
1339
1415
  const hiddenByCluster = /* @__PURE__ */ new Map();
1340
1416
  pool.begin();
1341
1417
  for (const p of placements) {
1342
- if (withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1418
+ const preview = p.item.item.preview || p.item.id === ctx.draggingId;
1419
+ if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1343
1420
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
1344
1421
  rec.count++;
1345
1422
  rec.start = Math.min(rec.start, p.startMin);
@@ -1348,7 +1425,7 @@ var TimeGridView = class {
1348
1425
  hiddenByCluster.set(p.cluster, rec);
1349
1426
  continue;
1350
1427
  }
1351
- const cols = withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1428
+ const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1352
1429
  const span = Math.min(p.span, cols - p.col);
1353
1430
  const left = p.col / cols;
1354
1431
  let width = span / cols;
@@ -2228,6 +2305,9 @@ var Calendar = class {
2228
2305
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2229
2306
  draggingId = null;
2230
2307
  dragRow = null;
2308
+ strip = null;
2309
+ stripSize = null;
2310
+ rebuilding = false;
2231
2311
  toolbarEl;
2232
2312
  viewHost;
2233
2313
  current = null;
@@ -2441,7 +2521,7 @@ var Calendar = class {
2441
2521
  }
2442
2522
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2443
2523
  hitTest(clientX, clientY) {
2444
- return this.current?.hit(clientX, clientY) ?? null;
2524
+ return this.hitAt(clientX, clientY);
2445
2525
  }
2446
2526
  /** Item elements currently rendered for an id. */
2447
2527
  elementsFor(id) {
@@ -2483,6 +2563,9 @@ var Calendar = class {
2483
2563
  this.unbindEvents();
2484
2564
  this.current?.destroy();
2485
2565
  this.current = null;
2566
+ this.stripSize?.disconnect();
2567
+ this.strip?.destroy();
2568
+ this.strip = null;
2486
2569
  this.root.remove();
2487
2570
  }
2488
2571
  /* ========================================================== view context */
@@ -2513,7 +2596,7 @@ var Calendar = class {
2513
2596
  return out;
2514
2597
  }
2515
2598
  isEditable(n, what) {
2516
- if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
2599
+ if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
2517
2600
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2518
2601
  const it = n.item;
2519
2602
  const base = it.editable ?? this.opts.editable;
@@ -2539,6 +2622,7 @@ var Calendar = class {
2539
2622
  createItemEl(meta) {
2540
2623
  const { n, variant } = meta;
2541
2624
  const it = n.item;
2625
+ const preview = meta.mirror || it.preview === true;
2542
2626
  const el = h(this.doc, "div", "tnc-item");
2543
2627
  el.classList.add(`tnc-item--${variant}`);
2544
2628
  if (it.kind) el.classList.add(`tnc-k-${it.kind.replace(/[^\w-]/g, "-")}`);
@@ -2548,12 +2632,12 @@ var Calendar = class {
2548
2632
  if (!meta.isStart) el.classList.add("is-cont-start");
2549
2633
  if (!meta.isEnd) el.classList.add("is-cont-end");
2550
2634
  if (n.allDay) el.classList.add("is-all-day");
2551
- if (meta.mirror || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2635
+ if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2552
2636
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2553
2637
  const indicator = variant === "indicator";
2554
- if (!meta.mirror && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2638
+ if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2555
2639
  if (this.draggingId === n.id) el.classList.add("is-dragging");
2556
- if (this.selectedIds.has(n.id) && !meta.mirror) el.classList.add("is-selected");
2640
+ if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
2557
2641
  el.classList.add(...classList(it.className));
2558
2642
  const arg = this.itemArg(el, meta);
2559
2643
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
@@ -2564,7 +2648,7 @@ var Calendar = class {
2564
2648
  if (it.backgroundColor) el.style.setProperty("--tnc-bg", it.backgroundColor);
2565
2649
  if (it.borderColor) el.style.setProperty("--tnc-border", it.borderColor);
2566
2650
  if (it.textColor) el.style.setProperty("--tnc-fg", it.textColor);
2567
- if (indicator) el.setAttribute("aria-hidden", "true");
2651
+ if (indicator || preview) el.setAttribute("aria-hidden", "true");
2568
2652
  else if (it.display !== "background") {
2569
2653
  el.tabIndex = 0;
2570
2654
  el.setAttribute("role", "button");
@@ -2581,7 +2665,7 @@ var Calendar = class {
2581
2665
  } else {
2582
2666
  el.append(this.defaultContent(meta));
2583
2667
  }
2584
- if (variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
2668
+ if (!preview && variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
2585
2669
  this.opts.itemDidMount?.(arg);
2586
2670
  return el;
2587
2671
  }
@@ -2590,7 +2674,7 @@ var Calendar = class {
2590
2674
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
2591
2675
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
2592
2676
  el.classList.toggle("is-conflict", this.conflicts.has(meta.n.id));
2593
- el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2677
+ el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
2594
2678
  }
2595
2679
  selectedSet() {
2596
2680
  const s = this.opts.selected;
@@ -2600,7 +2684,7 @@ var Calendar = class {
2600
2684
  this.selectedIds = this.selectedSet();
2601
2685
  for (const el of this.viewHost.querySelectorAll(".tnc-item")) {
2602
2686
  const meta = this.meta.get(el);
2603
- if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2687
+ if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
2604
2688
  }
2605
2689
  }
2606
2690
  destroyItemEl(el) {
@@ -2751,12 +2835,18 @@ var Calendar = class {
2751
2835
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2752
2836
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2753
2837
  if (navigated) this.listEnd = null;
2754
- const rangeChanged = this.applyRange();
2838
+ const prev = this.view;
2839
+ this.applyRange();
2755
2840
  this.conflicts = dependencyConflicts(this.store);
2756
2841
  this.root.className = "tnc";
2757
2842
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2758
2843
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2759
2844
  this.renderToolbar();
2845
+ this.rebuilding = true;
2846
+ this.syncStrip(navigated);
2847
+ this.rebuilding = false;
2848
+ this.view.dataRange = this.dataRangeFor(this.view.range);
2849
+ const rangeChanged = rangeMoved(prev, this.view);
2760
2850
  this.current?.destroy();
2761
2851
  this.current = this.createView();
2762
2852
  this.viewHost.replaceChildren(this.current.el);
@@ -2769,7 +2859,7 @@ var Calendar = class {
2769
2859
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2770
2860
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
2771
2861
  }
2772
- /** Sets the visible days and view info from the anchor; true when the range moved. */
2862
+ /** Sets the visible days and view info from the anchor. */
2773
2863
  applyRange() {
2774
2864
  const { startDay, endDay } = this.computeRange();
2775
2865
  const hidden = new Set(this.opts.hiddenDays);
@@ -2787,10 +2877,74 @@ var Calendar = class {
2787
2877
  start: new Date(toInstant(startDay, 0, this.zone)),
2788
2878
  end: new Date(toInstant(endDay, 0, this.zone))
2789
2879
  };
2790
- const prevView = this.view;
2791
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2880
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
2792
2881
  this.view.title = this.computeTitle(startDay, endDay);
2793
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2882
+ }
2883
+ /** The range, run on to the end of the strip's days so their dots are right. */
2884
+ dataRangeFor(range) {
2885
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
2886
+ if (end === range.endDay) return range;
2887
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
2888
+ }
2889
+ /** Adds, updates or removes the date strip for the current view. */
2890
+ syncStrip(navigated) {
2891
+ const { view, listAgenda, dateStrip } = this.opts;
2892
+ const list = view === "list";
2893
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
2894
+ if (!want) {
2895
+ if (!this.strip) return;
2896
+ this.stripSize?.disconnect();
2897
+ this.stripSize = null;
2898
+ this.strip.destroy();
2899
+ this.strip = null;
2900
+ this.root.style.removeProperty("--tnc-strip-h");
2901
+ return;
2902
+ }
2903
+ this.root.classList.add("tnc--has-strip");
2904
+ const { startDay, endDay } = this.view.range;
2905
+ if (!this.strip) {
2906
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
2907
+ this.strip = strip;
2908
+ this.root.insertBefore(strip.el, this.viewHost);
2909
+ const RO = this.win.ResizeObserver;
2910
+ if (RO) {
2911
+ this.stripSize = new RO(() => strip.measure());
2912
+ this.stripSize.observe(strip.el);
2913
+ }
2914
+ }
2915
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
2916
+ else if (navigated) this.strip.setFocus(startDay, null);
2917
+ this.strip.draw();
2918
+ }
2919
+ pickStripDay(day, ev) {
2920
+ this.dateClick(day, ev);
2921
+ if (this.agendaList()) {
2922
+ this.showListDay(day);
2923
+ return;
2924
+ }
2925
+ const { startDay, endDay } = this.view.range;
2926
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
2927
+ this.anchor = day;
2928
+ this.rebuild(true);
2929
+ }
2930
+ /** The strip moved to other days: load theirs too. */
2931
+ stripShown() {
2932
+ if (!this.strip || this.rebuilding || !this.view) return;
2933
+ const next = this.dataRangeFor(this.view.range);
2934
+ if (next.endDay === this.view.dataRange.endDay) return;
2935
+ this.view = { ...this.view, dataRange: next };
2936
+ this.opts.onRangeChange?.(this.view);
2937
+ }
2938
+ /** What is under a point, the date strip included. */
2939
+ hitAt(x, y) {
2940
+ const strip = this.strip;
2941
+ if (strip) {
2942
+ const el = this.doc.elementFromPoint(x, y);
2943
+ const day = el ? strip.dayAt(el) : null;
2944
+ strip.markTarget(day !== null && this.draggingId ? day : null);
2945
+ if (day !== null) return { kind: "day", day };
2946
+ }
2947
+ return this.current?.hit(x, y) ?? null;
2794
2948
  }
2795
2949
  agendaList() {
2796
2950
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -2803,6 +2957,7 @@ var Calendar = class {
2803
2957
  this.listEnd = end;
2804
2958
  this.applyRange();
2805
2959
  this.current?.render(null);
2960
+ this.strip?.draw();
2806
2961
  this.opts.onRangeChange?.(this.view);
2807
2962
  }
2808
2963
  showListDay(day) {
@@ -2924,7 +3079,7 @@ var Calendar = class {
2924
3079
  day: meta.day,
2925
3080
  isStart: meta.isStart,
2926
3081
  isEnd: meta.isEnd,
2927
- isMirror: meta.mirror,
3082
+ isMirror: meta.mirror || meta.n.item.preview === true,
2928
3083
  variant: meta.variant,
2929
3084
  timeText: meta.n.allDay ? "" : end ? `${start} \u2013 ${end}` : start
2930
3085
  };
@@ -3069,7 +3224,7 @@ var Calendar = class {
3069
3224
  }
3070
3225
  itemElFrom(target) {
3071
3226
  const el = target?.closest?.(".tnc-item");
3072
- return el && this.root.contains(el) && this.meta.has(el) ? el : null;
3227
+ return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3073
3228
  }
3074
3229
  onDocPointerDown = (ev) => {
3075
3230
  const t = ev.target;
@@ -3200,7 +3355,7 @@ var Calendar = class {
3200
3355
  };
3201
3356
  /** Proposal for the current pointer position. */
3202
3357
  track(s, ev) {
3203
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3358
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3204
3359
  if (s.mode === "external") {
3205
3360
  this.trackExternal(s, hit);
3206
3361
  return;
@@ -3326,7 +3481,7 @@ var Calendar = class {
3326
3481
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3327
3482
  }
3328
3483
  if (hit.kind === "day") {
3329
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
3484
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3330
3485
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3331
3486
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3332
3487
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -3461,7 +3616,7 @@ var Calendar = class {
3461
3616
  this.draggingId = null;
3462
3617
  this.dragRow = null;
3463
3618
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3464
- this.current?.dragEnded?.();
3619
+ this.strip?.markTarget(null);
3465
3620
  if (!s.active) return;
3466
3621
  if (s.mode === "select") {
3467
3622
  if (cancel) this.unselectSilently();
@@ -3758,6 +3913,9 @@ function scrollerOf(el, axis) {
3758
3913
  }
3759
3914
  return doc.scrollingElement;
3760
3915
  }
3916
+ function rangeMoved(prev, next) {
3917
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
3918
+ }
3761
3919
 
3762
3920
  export {
3763
3921
  Formatter,