@tasknotes/calendar 0.1.3 → 0.1.4

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",
@@ -642,12 +648,244 @@ var DayGridView = class {
642
648
  }
643
649
  };
644
650
 
651
+ // src/dom/views/list.ts
652
+ var LOAD_AHEAD = "0px 0px 600px 0px";
653
+ var ListView = class {
654
+ constructor(ctx) {
655
+ this.ctx = ctx;
656
+ const { doc, win, opts } = ctx;
657
+ this.el = h(doc, "div", "tnc-list");
658
+ this.scroller = h(doc, "div", "tnc-list-scroll");
659
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
660
+ this.scroller.append(this.body);
661
+ this.el.append(this.scroller);
662
+ this.agenda = opts.listAgenda;
663
+ if (!this.agenda) return;
664
+ this.el.classList.add("tnc-list--agenda");
665
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
666
+ this.scroller.append(this.sentinel);
667
+ const IO = win.IntersectionObserver;
668
+ if (IO) {
669
+ this.loader = new IO(
670
+ (entries) => {
671
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
672
+ },
673
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
674
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
675
+ );
676
+ }
677
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
678
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
679
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
680
+ }
681
+ ctx;
682
+ el;
683
+ scroller;
684
+ body;
685
+ agenda;
686
+ pools = /* @__PURE__ */ new Map();
687
+ sections = /* @__PURE__ */ new Map();
688
+ lead = null;
689
+ sentinel = null;
690
+ loader = null;
691
+ frame = null;
692
+ /** A picked day stays marked until the list is scrolled by hand. */
693
+ pinned = false;
694
+ itemsOn(day) {
695
+ const { store, opts } = this.ctx;
696
+ const out = store.spanningIn(day, day + 1, true);
697
+ const seen = new Set(out.map((n) => n.id));
698
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
699
+ 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));
700
+ }
701
+ render(_change) {
702
+ const { ctx, agenda } = this;
703
+ const { doc, days, opts } = ctx;
704
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
705
+ const leadItems = [];
706
+ const inLead = /* @__PURE__ */ new Set();
707
+ const keep = /* @__PURE__ */ new Set();
708
+ let prev = null;
709
+ for (const day of days) {
710
+ let items = this.itemsOn(day);
711
+ if (includes) {
712
+ items = items.filter((n) => {
713
+ if (!includes(n.item)) return true;
714
+ if (!inLead.has(n.id)) {
715
+ inLead.add(n.id);
716
+ leadItems.push(n);
717
+ }
718
+ return false;
719
+ });
720
+ }
721
+ if (!items.length && !agenda) continue;
722
+ keep.add(day);
723
+ let section = this.sections.get(day);
724
+ let pool = this.pools.get(day);
725
+ if (!section || !pool) {
726
+ const arg = ctx.dayArg(day);
727
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
728
+ const custom = opts.dayHeaderContent?.(arg);
729
+ const label = custom ?? ctx.fmt.longDate(day);
730
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
731
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
732
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
733
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
734
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
735
+ pool = new ItemPool(ctx, rows);
736
+ this.sections.set(day, section);
737
+ this.pools.set(day, pool);
738
+ }
739
+ section.classList.toggle("is-empty", !items.length);
740
+ this.fill(pool, items, () => day);
741
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
742
+ if (prev) prev.after(section);
743
+ else this.body.prepend(section);
744
+ }
745
+ prev = section;
746
+ }
747
+ for (const [day, section] of this.sections) {
748
+ if (keep.has(day)) continue;
749
+ this.pools.get(day)?.clear();
750
+ this.pools.delete(day);
751
+ section.remove();
752
+ this.sections.delete(day);
753
+ }
754
+ this.renderLead(leadItems);
755
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
756
+ if (!keep.size && !leadItems.length) {
757
+ if (!empty) {
758
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
759
+ this.body.append(empty);
760
+ }
761
+ } else empty?.remove();
762
+ this.ctx.strip?.draw();
763
+ if (this.loader && this.sentinel) {
764
+ this.loader.unobserve(this.sentinel);
765
+ this.loader.observe(this.sentinel);
766
+ }
767
+ }
768
+ /** Places items in a pool, keeping DOM order equal to item order. */
769
+ fill(pool, items, dayFor) {
770
+ pool.begin();
771
+ for (const n of items) {
772
+ const d = dayFor(n);
773
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
774
+ }
775
+ pool.end();
776
+ for (const n of items) {
777
+ const el = pool.elementsFor(n.id)[0];
778
+ if (el) pool.container.append(el);
779
+ }
780
+ }
781
+ renderLead(items) {
782
+ const { ctx } = this;
783
+ const { doc, opts } = ctx;
784
+ if (!items.length || opts.listLeadTitle === null) {
785
+ if (!this.lead) return;
786
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
787
+ this.lead.pool.clear();
788
+ this.lead.section.remove();
789
+ this.lead = null;
790
+ return;
791
+ }
792
+ if (!this.lead) {
793
+ const title = opts.listLeadTitle;
794
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
795
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
796
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
797
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
798
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
799
+ opts.listLeadDidMount?.({ el: slot });
800
+ }
801
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
802
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
803
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
804
+ }
805
+ addRow(day) {
806
+ const { ctx } = this;
807
+ const label = ctx.opts.listAddText ?? "";
808
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
809
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
810
+ return btn;
811
+ }
812
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
813
+ scrollToDay(day) {
814
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
815
+ if (!section) return;
816
+ this.ctx.strip?.measure();
817
+ section.scrollIntoView({ block: "start" });
818
+ this.ctx.strip?.setFocus(day);
819
+ this.pinned = true;
820
+ }
821
+ fold = () => {
822
+ this.pinned = false;
823
+ const strip = this.ctx.strip;
824
+ if (strip?.expanded) strip.setExpanded(false);
825
+ };
826
+ onScroll = () => {
827
+ if (this.frame !== null) return;
828
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
829
+ this.frame = null;
830
+ this.followTop();
831
+ });
832
+ };
833
+ /** Marks the first day not hidden under the strip. */
834
+ followTop() {
835
+ const strip = this.ctx.strip;
836
+ if (!strip || this.pinned) return;
837
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
838
+ for (const day of this.ctx.days) {
839
+ const section = this.sections.get(day);
840
+ if (section && section.getBoundingClientRect().bottom > edge) {
841
+ strip.setFocus(day);
842
+ return;
843
+ }
844
+ }
845
+ }
846
+ showSelection(_sel) {
847
+ }
848
+ hit(x, y) {
849
+ const el = this.ctx.doc.elementFromPoint(x, y);
850
+ const section = el?.closest(".tnc-list-section");
851
+ if (!section || !this.body.contains(section)) return null;
852
+ if (section === this.lead?.section) {
853
+ const item = el?.closest(".tnc-item");
854
+ const meta = item ? this.ctx.meta.get(item) : void 0;
855
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
856
+ }
857
+ const day = parseDay(section.dataset.date ?? "");
858
+ return day === null ? null : { kind: "day", day };
859
+ }
860
+ getScroll() {
861
+ return { top: this.scroller.scrollTop, left: 0 };
862
+ }
863
+ setScroll(s) {
864
+ this.scroller.scrollTop = s.top;
865
+ }
866
+ elementsFor(id) {
867
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
868
+ return pools.flatMap((p) => p.elementsFor(id));
869
+ }
870
+ destroy() {
871
+ const { win, opts } = this.ctx;
872
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
873
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
874
+ this.loader?.disconnect();
875
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
876
+ this.lead?.pool.clear();
877
+ for (const p of this.pools.values()) p.clear();
878
+ this.el.remove();
879
+ }
880
+ };
881
+
645
882
  // src/dom/views/strip.ts
646
883
  var SWIPE_PX = 40;
647
884
  var DateStrip = class {
648
- constructor(ctx, pick, focus) {
885
+ constructor(ctx, pick, focus, onShown) {
649
886
  this.ctx = ctx;
650
887
  this.pick = pick;
888
+ this.onShown = onShown;
651
889
  const { doc, text } = ctx;
652
890
  this.focus = focus;
653
891
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -659,8 +897,10 @@ var DateStrip = class {
659
897
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
660
898
  prev.addEventListener("click", () => this.step(-1));
661
899
  next.addEventListener("click", () => this.step(1));
900
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
901
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
662
902
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
663
- head.append(this.title, prev, next, this.slot);
903
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
664
904
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
665
905
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
666
906
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -677,27 +917,45 @@ var DateStrip = class {
677
917
  }
678
918
  ctx;
679
919
  pick;
920
+ onShown;
680
921
  el;
681
922
  title;
682
923
  dows;
683
924
  grid;
684
925
  slot;
926
+ todayBtn;
685
927
  /** Day at the top of the list. */
686
928
  focus;
687
929
  /** First day of the week shown, or the month shown when expanded. */
688
930
  shown;
689
931
  target = null;
932
+ /** Days tinted as on screen besides the focus (a time grid's range). */
933
+ range = null;
934
+ shownKey = "";
690
935
  swipe = null;
691
936
  swallowClick = false;
692
937
  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);
938
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
939
+ setFocus(day, range = this.range) {
940
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
941
+ if (day === this.focus && sameRange) return;
942
+ this.range = range;
943
+ if (day !== this.focus) {
944
+ this.focus = day;
945
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
946
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
947
+ }
699
948
  this.draw();
700
949
  }
950
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
951
+ measure() {
952
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
953
+ }
954
+ /** First day and end (exclusive) of the days on show. */
955
+ window() {
956
+ const days = this.days();
957
+ return { start: days[0], end: days[days.length - 1] + 1 };
958
+ }
701
959
  setExpanded(on) {
702
960
  if (on === this.expanded) return;
703
961
  this.expanded = on;
@@ -735,6 +993,7 @@ var DateStrip = class {
735
993
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
736
994
  this.title.setAttribute("aria-expanded", String(this.expanded));
737
995
  this.el.classList.toggle("is-expanded", this.expanded);
996
+ this.todayBtn.hidden = this.focus === ctx.today;
738
997
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
739
998
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
740
999
  const month = ymdOfDay(this.shown).month;
@@ -751,6 +1010,7 @@ var DateStrip = class {
751
1010
  "tnc-strip-day",
752
1011
  ...ctx.dayClasses(d),
753
1012
  d === this.focus ? "is-focus" : "",
1013
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
754
1014
  count ? "has-items" : "",
755
1015
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
756
1016
  d === this.target ? "is-drop-target" : ""
@@ -770,6 +1030,11 @@ var DateStrip = class {
770
1030
  this.grid.replaceChildren(...cells);
771
1031
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
772
1032
  if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1033
+ const key = `${days[0]}:${days.length}`;
1034
+ if (key !== this.shownKey) {
1035
+ this.shownKey = key;
1036
+ this.onShown();
1037
+ }
773
1038
  }
774
1039
  onClick = (ev) => {
775
1040
  const day = this.dayAt(ev.target);
@@ -824,205 +1089,6 @@ var DateStrip = class {
824
1089
  }
825
1090
  };
826
1091
 
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
1092
  // src/dom/views/timegrid.ts
1027
1093
  var BAR_GAP2 = 2;
1028
1094
  var TimeGridView = class {
@@ -2228,6 +2294,9 @@ var Calendar = class {
2228
2294
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2229
2295
  draggingId = null;
2230
2296
  dragRow = null;
2297
+ strip = null;
2298
+ stripSize = null;
2299
+ rebuilding = false;
2231
2300
  toolbarEl;
2232
2301
  viewHost;
2233
2302
  current = null;
@@ -2441,7 +2510,7 @@ var Calendar = class {
2441
2510
  }
2442
2511
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2443
2512
  hitTest(clientX, clientY) {
2444
- return this.current?.hit(clientX, clientY) ?? null;
2513
+ return this.hitAt(clientX, clientY);
2445
2514
  }
2446
2515
  /** Item elements currently rendered for an id. */
2447
2516
  elementsFor(id) {
@@ -2483,6 +2552,9 @@ var Calendar = class {
2483
2552
  this.unbindEvents();
2484
2553
  this.current?.destroy();
2485
2554
  this.current = null;
2555
+ this.stripSize?.disconnect();
2556
+ this.strip?.destroy();
2557
+ this.strip = null;
2486
2558
  this.root.remove();
2487
2559
  }
2488
2560
  /* ========================================================== view context */
@@ -2751,12 +2823,18 @@ var Calendar = class {
2751
2823
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2752
2824
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2753
2825
  if (navigated) this.listEnd = null;
2754
- const rangeChanged = this.applyRange();
2826
+ const prev = this.view;
2827
+ this.applyRange();
2755
2828
  this.conflicts = dependencyConflicts(this.store);
2756
2829
  this.root.className = "tnc";
2757
2830
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2758
2831
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2759
2832
  this.renderToolbar();
2833
+ this.rebuilding = true;
2834
+ this.syncStrip(navigated);
2835
+ this.rebuilding = false;
2836
+ this.view.dataRange = this.dataRangeFor(this.view.range);
2837
+ const rangeChanged = rangeMoved(prev, this.view);
2760
2838
  this.current?.destroy();
2761
2839
  this.current = this.createView();
2762
2840
  this.viewHost.replaceChildren(this.current.el);
@@ -2769,7 +2847,7 @@ var Calendar = class {
2769
2847
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2770
2848
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
2771
2849
  }
2772
- /** Sets the visible days and view info from the anchor; true when the range moved. */
2850
+ /** Sets the visible days and view info from the anchor. */
2773
2851
  applyRange() {
2774
2852
  const { startDay, endDay } = this.computeRange();
2775
2853
  const hidden = new Set(this.opts.hiddenDays);
@@ -2787,10 +2865,74 @@ var Calendar = class {
2787
2865
  start: new Date(toInstant(startDay, 0, this.zone)),
2788
2866
  end: new Date(toInstant(endDay, 0, this.zone))
2789
2867
  };
2790
- const prevView = this.view;
2791
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2868
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
2792
2869
  this.view.title = this.computeTitle(startDay, endDay);
2793
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2870
+ }
2871
+ /** The range, run on to the end of the strip's days so their dots are right. */
2872
+ dataRangeFor(range) {
2873
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
2874
+ if (end === range.endDay) return range;
2875
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
2876
+ }
2877
+ /** Adds, updates or removes the date strip for the current view. */
2878
+ syncStrip(navigated) {
2879
+ const { view, listAgenda, dateStrip } = this.opts;
2880
+ const list = view === "list";
2881
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
2882
+ if (!want) {
2883
+ if (!this.strip) return;
2884
+ this.stripSize?.disconnect();
2885
+ this.stripSize = null;
2886
+ this.strip.destroy();
2887
+ this.strip = null;
2888
+ this.root.style.removeProperty("--tnc-strip-h");
2889
+ return;
2890
+ }
2891
+ this.root.classList.add("tnc--has-strip");
2892
+ const { startDay, endDay } = this.view.range;
2893
+ if (!this.strip) {
2894
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
2895
+ this.strip = strip;
2896
+ this.root.insertBefore(strip.el, this.viewHost);
2897
+ const RO = this.win.ResizeObserver;
2898
+ if (RO) {
2899
+ this.stripSize = new RO(() => strip.measure());
2900
+ this.stripSize.observe(strip.el);
2901
+ }
2902
+ }
2903
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
2904
+ else if (navigated) this.strip.setFocus(startDay, null);
2905
+ this.strip.draw();
2906
+ }
2907
+ pickStripDay(day, ev) {
2908
+ this.dateClick(day, ev);
2909
+ if (this.agendaList()) {
2910
+ this.showListDay(day);
2911
+ return;
2912
+ }
2913
+ const { startDay, endDay } = this.view.range;
2914
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
2915
+ this.anchor = day;
2916
+ this.rebuild(true);
2917
+ }
2918
+ /** The strip moved to other days: load theirs too. */
2919
+ stripShown() {
2920
+ if (!this.strip || this.rebuilding || !this.view) return;
2921
+ const next = this.dataRangeFor(this.view.range);
2922
+ if (next.endDay === this.view.dataRange.endDay) return;
2923
+ this.view = { ...this.view, dataRange: next };
2924
+ this.opts.onRangeChange?.(this.view);
2925
+ }
2926
+ /** What is under a point, the date strip included. */
2927
+ hitAt(x, y) {
2928
+ const strip = this.strip;
2929
+ if (strip) {
2930
+ const el = this.doc.elementFromPoint(x, y);
2931
+ const day = el ? strip.dayAt(el) : null;
2932
+ strip.markTarget(day !== null && this.draggingId ? day : null);
2933
+ if (day !== null) return { kind: "day", day };
2934
+ }
2935
+ return this.current?.hit(x, y) ?? null;
2794
2936
  }
2795
2937
  agendaList() {
2796
2938
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -2803,6 +2945,7 @@ var Calendar = class {
2803
2945
  this.listEnd = end;
2804
2946
  this.applyRange();
2805
2947
  this.current?.render(null);
2948
+ this.strip?.draw();
2806
2949
  this.opts.onRangeChange?.(this.view);
2807
2950
  }
2808
2951
  showListDay(day) {
@@ -3200,7 +3343,7 @@ var Calendar = class {
3200
3343
  };
3201
3344
  /** Proposal for the current pointer position. */
3202
3345
  track(s, ev) {
3203
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3346
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3204
3347
  if (s.mode === "external") {
3205
3348
  this.trackExternal(s, hit);
3206
3349
  return;
@@ -3326,7 +3469,7 @@ var Calendar = class {
3326
3469
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3327
3470
  }
3328
3471
  if (hit.kind === "day") {
3329
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
3472
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3330
3473
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3331
3474
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3332
3475
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -3461,7 +3604,7 @@ var Calendar = class {
3461
3604
  this.draggingId = null;
3462
3605
  this.dragRow = null;
3463
3606
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3464
- this.current?.dragEnded?.();
3607
+ this.strip?.markTarget(null);
3465
3608
  if (!s.active) return;
3466
3609
  if (s.mode === "select") {
3467
3610
  if (cancel) this.unselectSilently();
@@ -3758,6 +3901,9 @@ function scrollerOf(el, axis) {
3758
3901
  }
3759
3902
  return doc.scrollingElement;
3760
3903
  }
3904
+ function rangeMoved(prev, next) {
3905
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
3906
+ }
3761
3907
 
3762
3908
  export {
3763
3909
  Formatter,