@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.
- package/CHANGELOG.md +13 -0
- package/README.md +6 -2
- package/dist/calendar.css +76 -5
- package/dist/cjs/index.cjs +362 -216
- package/dist/cjs/react.cjs +362 -216
- package/dist/esm/{chunk-XHPZ7K4W.js → chunk-26DEUGQH.js} +362 -216
- package/dist/esm/index.js +1 -1
- package/dist/esm/react.js +1 -1
- package/dist/types/dom/calendar.d.cts +14 -1
- package/dist/types/dom/calendar.d.ts +14 -1
- package/dist/types/dom/context.d.cts +4 -3
- package/dist/types/dom/context.d.ts +4 -3
- package/dist/types/dom/options.d.cts +33 -2
- package/dist/types/dom/options.d.ts +33 -2
- package/dist/types/dom/views/list.d.cts +10 -8
- package/dist/types/dom/views/list.d.ts +10 -8
- package/dist/types/dom/views/strip.d.cts +26 -6
- package/dist/types/dom/views/strip.d.ts +26 -6
- package/package.json +1 -1
|
@@ -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
|
|
694
|
-
setFocus(day) {
|
|
695
|
-
|
|
696
|
-
this.focus
|
|
697
|
-
|
|
698
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 === "
|
|
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.
|
|
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,
|