@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
package/dist/cjs/index.cjs
CHANGED
|
@@ -949,6 +949,9 @@ function defaultOptions() {
|
|
|
949
949
|
listDayCount: 7,
|
|
950
950
|
listStart: "date",
|
|
951
951
|
listAgenda: false,
|
|
952
|
+
dateStrip: false,
|
|
953
|
+
listLeadTitle: null,
|
|
954
|
+
listAddText: null,
|
|
952
955
|
monthDisplay: "full",
|
|
953
956
|
timeZone: "local",
|
|
954
957
|
locale: void 0,
|
|
@@ -999,6 +1002,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
|
|
|
999
1002
|
"listDayCount",
|
|
1000
1003
|
"listStart",
|
|
1001
1004
|
"listAgenda",
|
|
1005
|
+
"dateStrip",
|
|
1006
|
+
"listLeadTitle",
|
|
1007
|
+
"listAddText",
|
|
1002
1008
|
"monthDisplay",
|
|
1003
1009
|
"timeZone",
|
|
1004
1010
|
"locale",
|
|
@@ -1340,12 +1346,244 @@ var DayGridView = class {
|
|
|
1340
1346
|
}
|
|
1341
1347
|
};
|
|
1342
1348
|
|
|
1349
|
+
// src/dom/views/list.ts
|
|
1350
|
+
var LOAD_AHEAD = "0px 0px 600px 0px";
|
|
1351
|
+
var ListView = class {
|
|
1352
|
+
constructor(ctx) {
|
|
1353
|
+
this.ctx = ctx;
|
|
1354
|
+
const { doc, win, opts } = ctx;
|
|
1355
|
+
this.el = h(doc, "div", "tnc-list");
|
|
1356
|
+
this.scroller = h(doc, "div", "tnc-list-scroll");
|
|
1357
|
+
this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
|
|
1358
|
+
this.scroller.append(this.body);
|
|
1359
|
+
this.el.append(this.scroller);
|
|
1360
|
+
this.agenda = opts.listAgenda;
|
|
1361
|
+
if (!this.agenda) return;
|
|
1362
|
+
this.el.classList.add("tnc-list--agenda");
|
|
1363
|
+
this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
|
|
1364
|
+
this.scroller.append(this.sentinel);
|
|
1365
|
+
const IO = win.IntersectionObserver;
|
|
1366
|
+
if (IO) {
|
|
1367
|
+
this.loader = new IO(
|
|
1368
|
+
(entries) => {
|
|
1369
|
+
if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
|
|
1370
|
+
},
|
|
1371
|
+
// Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
|
|
1372
|
+
{ root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
|
|
1376
|
+
this.scroller.addEventListener("wheel", this.fold, { passive: true });
|
|
1377
|
+
this.scroller.addEventListener("touchmove", this.fold, { passive: true });
|
|
1378
|
+
}
|
|
1379
|
+
ctx;
|
|
1380
|
+
el;
|
|
1381
|
+
scroller;
|
|
1382
|
+
body;
|
|
1383
|
+
agenda;
|
|
1384
|
+
pools = /* @__PURE__ */ new Map();
|
|
1385
|
+
sections = /* @__PURE__ */ new Map();
|
|
1386
|
+
lead = null;
|
|
1387
|
+
sentinel = null;
|
|
1388
|
+
loader = null;
|
|
1389
|
+
frame = null;
|
|
1390
|
+
/** A picked day stays marked until the list is scrolled by hand. */
|
|
1391
|
+
pinned = false;
|
|
1392
|
+
itemsOn(day) {
|
|
1393
|
+
const { store, opts } = this.ctx;
|
|
1394
|
+
const out = store.spanningIn(day, day + 1, true);
|
|
1395
|
+
const seen = new Set(out.map((n) => n.id));
|
|
1396
|
+
for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
|
|
1397
|
+
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));
|
|
1398
|
+
}
|
|
1399
|
+
render(_change) {
|
|
1400
|
+
const { ctx, agenda } = this;
|
|
1401
|
+
const { doc, days, opts } = ctx;
|
|
1402
|
+
const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
|
|
1403
|
+
const leadItems = [];
|
|
1404
|
+
const inLead = /* @__PURE__ */ new Set();
|
|
1405
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1406
|
+
let prev = null;
|
|
1407
|
+
for (const day of days) {
|
|
1408
|
+
let items = this.itemsOn(day);
|
|
1409
|
+
if (includes) {
|
|
1410
|
+
items = items.filter((n) => {
|
|
1411
|
+
if (!includes(n.item)) return true;
|
|
1412
|
+
if (!inLead.has(n.id)) {
|
|
1413
|
+
inLead.add(n.id);
|
|
1414
|
+
leadItems.push(n);
|
|
1415
|
+
}
|
|
1416
|
+
return false;
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
if (!items.length && !agenda) continue;
|
|
1420
|
+
keep.add(day);
|
|
1421
|
+
let section = this.sections.get(day);
|
|
1422
|
+
let pool = this.pools.get(day);
|
|
1423
|
+
if (!section || !pool) {
|
|
1424
|
+
const arg = ctx.dayArg(day);
|
|
1425
|
+
const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
|
|
1426
|
+
const custom = opts.dayHeaderContent?.(arg);
|
|
1427
|
+
const label = custom ?? ctx.fmt.longDate(day);
|
|
1428
|
+
heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
|
|
1429
|
+
const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1430
|
+
section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
|
|
1431
|
+
if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
|
|
1432
|
+
opts.dayHeaderDidMount?.({ ...arg, el: heading });
|
|
1433
|
+
pool = new ItemPool(ctx, rows);
|
|
1434
|
+
this.sections.set(day, section);
|
|
1435
|
+
this.pools.set(day, pool);
|
|
1436
|
+
}
|
|
1437
|
+
section.classList.toggle("is-empty", !items.length);
|
|
1438
|
+
this.fill(pool, items, () => day);
|
|
1439
|
+
if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
|
|
1440
|
+
if (prev) prev.after(section);
|
|
1441
|
+
else this.body.prepend(section);
|
|
1442
|
+
}
|
|
1443
|
+
prev = section;
|
|
1444
|
+
}
|
|
1445
|
+
for (const [day, section] of this.sections) {
|
|
1446
|
+
if (keep.has(day)) continue;
|
|
1447
|
+
this.pools.get(day)?.clear();
|
|
1448
|
+
this.pools.delete(day);
|
|
1449
|
+
section.remove();
|
|
1450
|
+
this.sections.delete(day);
|
|
1451
|
+
}
|
|
1452
|
+
this.renderLead(leadItems);
|
|
1453
|
+
let empty = this.body.querySelector(":scope > .tnc-list-empty");
|
|
1454
|
+
if (!keep.size && !leadItems.length) {
|
|
1455
|
+
if (!empty) {
|
|
1456
|
+
empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
|
|
1457
|
+
this.body.append(empty);
|
|
1458
|
+
}
|
|
1459
|
+
} else empty?.remove();
|
|
1460
|
+
this.ctx.strip?.draw();
|
|
1461
|
+
if (this.loader && this.sentinel) {
|
|
1462
|
+
this.loader.unobserve(this.sentinel);
|
|
1463
|
+
this.loader.observe(this.sentinel);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
/** Places items in a pool, keeping DOM order equal to item order. */
|
|
1467
|
+
fill(pool, items, dayFor) {
|
|
1468
|
+
pool.begin();
|
|
1469
|
+
for (const n of items) {
|
|
1470
|
+
const d = dayFor(n);
|
|
1471
|
+
pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
|
|
1472
|
+
}
|
|
1473
|
+
pool.end();
|
|
1474
|
+
for (const n of items) {
|
|
1475
|
+
const el = pool.elementsFor(n.id)[0];
|
|
1476
|
+
if (el) pool.container.append(el);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
renderLead(items) {
|
|
1480
|
+
const { ctx } = this;
|
|
1481
|
+
const { doc, opts } = ctx;
|
|
1482
|
+
if (!items.length || opts.listLeadTitle === null) {
|
|
1483
|
+
if (!this.lead) return;
|
|
1484
|
+
opts.listLeadWillUnmount?.({ el: this.lead.slot });
|
|
1485
|
+
this.lead.pool.clear();
|
|
1486
|
+
this.lead.section.remove();
|
|
1487
|
+
this.lead = null;
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (!this.lead) {
|
|
1491
|
+
const title = opts.listLeadTitle;
|
|
1492
|
+
const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
|
|
1493
|
+
const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
|
|
1494
|
+
const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1495
|
+
const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
|
|
1496
|
+
this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
|
|
1497
|
+
opts.listLeadDidMount?.({ el: slot });
|
|
1498
|
+
}
|
|
1499
|
+
if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
|
|
1500
|
+
const first = ctx.days[0] ?? ctx.view.range.startDay;
|
|
1501
|
+
this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
|
|
1502
|
+
}
|
|
1503
|
+
addRow(day) {
|
|
1504
|
+
const { ctx } = this;
|
|
1505
|
+
const label = ctx.opts.listAddText ?? "";
|
|
1506
|
+
const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
|
|
1507
|
+
btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
|
|
1508
|
+
return btn;
|
|
1509
|
+
}
|
|
1510
|
+
/** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
|
|
1511
|
+
scrollToDay(day) {
|
|
1512
|
+
const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
|
|
1513
|
+
if (!section) return;
|
|
1514
|
+
this.ctx.strip?.measure();
|
|
1515
|
+
section.scrollIntoView({ block: "start" });
|
|
1516
|
+
this.ctx.strip?.setFocus(day);
|
|
1517
|
+
this.pinned = true;
|
|
1518
|
+
}
|
|
1519
|
+
fold = () => {
|
|
1520
|
+
this.pinned = false;
|
|
1521
|
+
const strip = this.ctx.strip;
|
|
1522
|
+
if (strip?.expanded) strip.setExpanded(false);
|
|
1523
|
+
};
|
|
1524
|
+
onScroll = () => {
|
|
1525
|
+
if (this.frame !== null) return;
|
|
1526
|
+
this.frame = this.ctx.win.requestAnimationFrame(() => {
|
|
1527
|
+
this.frame = null;
|
|
1528
|
+
this.followTop();
|
|
1529
|
+
});
|
|
1530
|
+
};
|
|
1531
|
+
/** Marks the first day not hidden under the strip. */
|
|
1532
|
+
followTop() {
|
|
1533
|
+
const strip = this.ctx.strip;
|
|
1534
|
+
if (!strip || this.pinned) return;
|
|
1535
|
+
const edge = strip.el.getBoundingClientRect().bottom + 1;
|
|
1536
|
+
for (const day of this.ctx.days) {
|
|
1537
|
+
const section = this.sections.get(day);
|
|
1538
|
+
if (section && section.getBoundingClientRect().bottom > edge) {
|
|
1539
|
+
strip.setFocus(day);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
showSelection(_sel) {
|
|
1545
|
+
}
|
|
1546
|
+
hit(x, y) {
|
|
1547
|
+
const el = this.ctx.doc.elementFromPoint(x, y);
|
|
1548
|
+
const section = el?.closest(".tnc-list-section");
|
|
1549
|
+
if (!section || !this.body.contains(section)) return null;
|
|
1550
|
+
if (section === this.lead?.section) {
|
|
1551
|
+
const item = el?.closest(".tnc-item");
|
|
1552
|
+
const meta = item ? this.ctx.meta.get(item) : void 0;
|
|
1553
|
+
return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
|
|
1554
|
+
}
|
|
1555
|
+
const day = parseDay(section.dataset.date ?? "");
|
|
1556
|
+
return day === null ? null : { kind: "day", day };
|
|
1557
|
+
}
|
|
1558
|
+
getScroll() {
|
|
1559
|
+
return { top: this.scroller.scrollTop, left: 0 };
|
|
1560
|
+
}
|
|
1561
|
+
setScroll(s) {
|
|
1562
|
+
this.scroller.scrollTop = s.top;
|
|
1563
|
+
}
|
|
1564
|
+
elementsFor(id) {
|
|
1565
|
+
const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
|
|
1566
|
+
return pools.flatMap((p) => p.elementsFor(id));
|
|
1567
|
+
}
|
|
1568
|
+
destroy() {
|
|
1569
|
+
const { win, opts } = this.ctx;
|
|
1570
|
+
win.removeEventListener("scroll", this.onScroll, { capture: true });
|
|
1571
|
+
if (this.frame !== null) win.cancelAnimationFrame(this.frame);
|
|
1572
|
+
this.loader?.disconnect();
|
|
1573
|
+
if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
|
|
1574
|
+
this.lead?.pool.clear();
|
|
1575
|
+
for (const p of this.pools.values()) p.clear();
|
|
1576
|
+
this.el.remove();
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1343
1580
|
// src/dom/views/strip.ts
|
|
1344
1581
|
var SWIPE_PX = 40;
|
|
1345
1582
|
var DateStrip = class {
|
|
1346
|
-
constructor(ctx, pick, focus) {
|
|
1583
|
+
constructor(ctx, pick, focus, onShown) {
|
|
1347
1584
|
this.ctx = ctx;
|
|
1348
1585
|
this.pick = pick;
|
|
1586
|
+
this.onShown = onShown;
|
|
1349
1587
|
const { doc, text } = ctx;
|
|
1350
1588
|
this.focus = focus;
|
|
1351
1589
|
this.shown = startOfWeek(focus, ctx.firstDay);
|
|
@@ -1357,8 +1595,10 @@ var DateStrip = class {
|
|
|
1357
1595
|
const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
|
|
1358
1596
|
prev.addEventListener("click", () => this.step(-1));
|
|
1359
1597
|
next.addEventListener("click", () => this.step(1));
|
|
1598
|
+
this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
|
|
1599
|
+
this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
|
|
1360
1600
|
this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
|
|
1361
|
-
head.append(this.title, prev, next, this.slot);
|
|
1601
|
+
head.append(this.title, this.todayBtn, prev, next, this.slot);
|
|
1362
1602
|
this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
|
|
1363
1603
|
this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
|
|
1364
1604
|
const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
|
|
@@ -1375,27 +1615,45 @@ var DateStrip = class {
|
|
|
1375
1615
|
}
|
|
1376
1616
|
ctx;
|
|
1377
1617
|
pick;
|
|
1618
|
+
onShown;
|
|
1378
1619
|
el;
|
|
1379
1620
|
title;
|
|
1380
1621
|
dows;
|
|
1381
1622
|
grid;
|
|
1382
1623
|
slot;
|
|
1624
|
+
todayBtn;
|
|
1383
1625
|
/** Day at the top of the list. */
|
|
1384
1626
|
focus;
|
|
1385
1627
|
/** First day of the week shown, or the month shown when expanded. */
|
|
1386
1628
|
shown;
|
|
1387
1629
|
target = null;
|
|
1630
|
+
/** Days tinted as on screen besides the focus (a time grid's range). */
|
|
1631
|
+
range = null;
|
|
1632
|
+
shownKey = "";
|
|
1388
1633
|
swipe = null;
|
|
1389
1634
|
swallowClick = false;
|
|
1390
1635
|
expanded = false;
|
|
1391
|
-
/** The list scrolled: mark
|
|
1392
|
-
setFocus(day) {
|
|
1393
|
-
|
|
1394
|
-
this.focus
|
|
1395
|
-
|
|
1396
|
-
|
|
1636
|
+
/** The list scrolled or the grid moved: mark the day and keep it in view. */
|
|
1637
|
+
setFocus(day, range = this.range) {
|
|
1638
|
+
const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
|
|
1639
|
+
if (day === this.focus && sameRange) return;
|
|
1640
|
+
this.range = range;
|
|
1641
|
+
if (day !== this.focus) {
|
|
1642
|
+
this.focus = day;
|
|
1643
|
+
if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
|
|
1644
|
+
else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
|
|
1645
|
+
}
|
|
1397
1646
|
this.draw();
|
|
1398
1647
|
}
|
|
1648
|
+
/** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
|
|
1649
|
+
measure() {
|
|
1650
|
+
this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
|
|
1651
|
+
}
|
|
1652
|
+
/** First day and end (exclusive) of the days on show. */
|
|
1653
|
+
window() {
|
|
1654
|
+
const days = this.days();
|
|
1655
|
+
return { start: days[0], end: days[days.length - 1] + 1 };
|
|
1656
|
+
}
|
|
1399
1657
|
setExpanded(on) {
|
|
1400
1658
|
if (on === this.expanded) return;
|
|
1401
1659
|
this.expanded = on;
|
|
@@ -1433,6 +1691,7 @@ var DateStrip = class {
|
|
|
1433
1691
|
this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
|
|
1434
1692
|
this.title.setAttribute("aria-expanded", String(this.expanded));
|
|
1435
1693
|
this.el.classList.toggle("is-expanded", this.expanded);
|
|
1694
|
+
this.todayBtn.hidden = this.focus === ctx.today;
|
|
1436
1695
|
this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
|
|
1437
1696
|
const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
|
|
1438
1697
|
const month = ymdOfDay(this.shown).month;
|
|
@@ -1449,6 +1708,7 @@ var DateStrip = class {
|
|
|
1449
1708
|
"tnc-strip-day",
|
|
1450
1709
|
...ctx.dayClasses(d),
|
|
1451
1710
|
d === this.focus ? "is-focus" : "",
|
|
1711
|
+
d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
|
|
1452
1712
|
count ? "has-items" : "",
|
|
1453
1713
|
this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
|
|
1454
1714
|
d === this.target ? "is-drop-target" : ""
|
|
@@ -1468,6 +1728,11 @@ var DateStrip = class {
|
|
|
1468
1728
|
this.grid.replaceChildren(...cells);
|
|
1469
1729
|
this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
|
|
1470
1730
|
if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
|
|
1731
|
+
const key = `${days[0]}:${days.length}`;
|
|
1732
|
+
if (key !== this.shownKey) {
|
|
1733
|
+
this.shownKey = key;
|
|
1734
|
+
this.onShown();
|
|
1735
|
+
}
|
|
1471
1736
|
}
|
|
1472
1737
|
onClick = (ev) => {
|
|
1473
1738
|
const day = this.dayAt(ev.target);
|
|
@@ -1522,205 +1787,6 @@ var DateStrip = class {
|
|
|
1522
1787
|
}
|
|
1523
1788
|
};
|
|
1524
1789
|
|
|
1525
|
-
// src/dom/views/list.ts
|
|
1526
|
-
var LOAD_AHEAD = "0px 0px 600px 0px";
|
|
1527
|
-
var ListView = class {
|
|
1528
|
-
constructor(ctx) {
|
|
1529
|
-
this.ctx = ctx;
|
|
1530
|
-
const { doc, win, opts } = ctx;
|
|
1531
|
-
this.el = h(doc, "div", "tnc-list");
|
|
1532
|
-
this.scroller = h(doc, "div", "tnc-list-scroll");
|
|
1533
|
-
this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
|
|
1534
|
-
this.scroller.append(this.body);
|
|
1535
|
-
this.el.append(this.scroller);
|
|
1536
|
-
if (!opts.listAgenda) return;
|
|
1537
|
-
this.el.classList.add("tnc-list--agenda");
|
|
1538
|
-
this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
|
|
1539
|
-
this.el.prepend(this.strip.el);
|
|
1540
|
-
this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
|
|
1541
|
-
this.scroller.append(this.sentinel);
|
|
1542
|
-
const globals = win;
|
|
1543
|
-
const IO = globals.IntersectionObserver;
|
|
1544
|
-
if (IO) {
|
|
1545
|
-
this.loader = new IO(
|
|
1546
|
-
(entries) => {
|
|
1547
|
-
if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
|
|
1548
|
-
},
|
|
1549
|
-
// Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
|
|
1550
|
-
{ root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
|
|
1551
|
-
);
|
|
1552
|
-
}
|
|
1553
|
-
const RO = globals.ResizeObserver;
|
|
1554
|
-
if (RO) {
|
|
1555
|
-
const strip = this.strip.el;
|
|
1556
|
-
this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
|
|
1557
|
-
this.stripSize.observe(strip);
|
|
1558
|
-
}
|
|
1559
|
-
win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
|
|
1560
|
-
this.scroller.addEventListener("wheel", this.fold, { passive: true });
|
|
1561
|
-
this.scroller.addEventListener("touchmove", this.fold, { passive: true });
|
|
1562
|
-
}
|
|
1563
|
-
ctx;
|
|
1564
|
-
el;
|
|
1565
|
-
scroller;
|
|
1566
|
-
body;
|
|
1567
|
-
pools = /* @__PURE__ */ new Map();
|
|
1568
|
-
sections = /* @__PURE__ */ new Map();
|
|
1569
|
-
strip = null;
|
|
1570
|
-
sentinel = null;
|
|
1571
|
-
loader = null;
|
|
1572
|
-
stripSize = null;
|
|
1573
|
-
frame = null;
|
|
1574
|
-
/** A picked day stays marked until the list is scrolled by hand. */
|
|
1575
|
-
pinned = false;
|
|
1576
|
-
itemsOn(day) {
|
|
1577
|
-
const { store, opts } = this.ctx;
|
|
1578
|
-
const out = store.spanningIn(day, day + 1, true);
|
|
1579
|
-
const seen = new Set(out.map((n) => n.id));
|
|
1580
|
-
for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
|
|
1581
|
-
return out.filter((n) => n.item.display !== "background" && n.item.display !== "none" && (opts.allDaySlot || !n.allDay)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.ctx.order(a, b));
|
|
1582
|
-
}
|
|
1583
|
-
render(_change) {
|
|
1584
|
-
const { ctx } = this;
|
|
1585
|
-
const { doc, days } = ctx;
|
|
1586
|
-
const agenda = !!this.strip;
|
|
1587
|
-
const keep = /* @__PURE__ */ new Set();
|
|
1588
|
-
let prev = null;
|
|
1589
|
-
for (const day of days) {
|
|
1590
|
-
const items = this.itemsOn(day);
|
|
1591
|
-
if (!items.length && !agenda) continue;
|
|
1592
|
-
keep.add(day);
|
|
1593
|
-
let section = this.sections.get(day);
|
|
1594
|
-
let pool = this.pools.get(day);
|
|
1595
|
-
if (!section || !pool) {
|
|
1596
|
-
const arg = ctx.dayArg(day);
|
|
1597
|
-
const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
|
|
1598
|
-
const custom = ctx.opts.dayHeaderContent?.(arg);
|
|
1599
|
-
const label = custom ?? ctx.fmt.longDate(day);
|
|
1600
|
-
heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
|
|
1601
|
-
const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1602
|
-
section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
|
|
1603
|
-
ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
|
|
1604
|
-
pool = new ItemPool(ctx, rows2);
|
|
1605
|
-
this.sections.set(day, section);
|
|
1606
|
-
this.pools.set(day, pool);
|
|
1607
|
-
}
|
|
1608
|
-
section.classList.toggle("is-empty", !items.length);
|
|
1609
|
-
pool.begin();
|
|
1610
|
-
for (const n of items) {
|
|
1611
|
-
pool.place(n.id, {
|
|
1612
|
-
n,
|
|
1613
|
-
day,
|
|
1614
|
-
isStart: n.startDay === day,
|
|
1615
|
-
isEnd: n.endDay === day + 1,
|
|
1616
|
-
variant: "list",
|
|
1617
|
-
mirror: false
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
pool.end();
|
|
1621
|
-
const rows = pool.container;
|
|
1622
|
-
for (const n of items) {
|
|
1623
|
-
const el = pool.elementsFor(n.id)[0];
|
|
1624
|
-
if (el) rows.append(el);
|
|
1625
|
-
}
|
|
1626
|
-
if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
|
|
1627
|
-
if (prev) prev.after(section);
|
|
1628
|
-
else this.body.prepend(section);
|
|
1629
|
-
}
|
|
1630
|
-
prev = section;
|
|
1631
|
-
}
|
|
1632
|
-
for (const [day, section] of this.sections) {
|
|
1633
|
-
if (keep.has(day)) continue;
|
|
1634
|
-
this.pools.get(day)?.clear();
|
|
1635
|
-
this.pools.delete(day);
|
|
1636
|
-
section.remove();
|
|
1637
|
-
this.sections.delete(day);
|
|
1638
|
-
}
|
|
1639
|
-
let empty = this.body.querySelector(":scope > .tnc-list-empty");
|
|
1640
|
-
if (!keep.size) {
|
|
1641
|
-
if (!empty) {
|
|
1642
|
-
empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
|
|
1643
|
-
this.body.append(empty);
|
|
1644
|
-
}
|
|
1645
|
-
} else empty?.remove();
|
|
1646
|
-
this.strip?.draw();
|
|
1647
|
-
if (this.loader && this.sentinel) {
|
|
1648
|
-
this.loader.unobserve(this.sentinel);
|
|
1649
|
-
this.loader.observe(this.sentinel);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
/** A strip day: the host hears of it, and the list brings it to the top. */
|
|
1653
|
-
pick(day, ev) {
|
|
1654
|
-
this.ctx.dateClick(day, ev);
|
|
1655
|
-
this.ctx.showListDay(day);
|
|
1656
|
-
}
|
|
1657
|
-
/** Brings a day's section to the top, under the strip. */
|
|
1658
|
-
scrollToDay(day) {
|
|
1659
|
-
const section = this.sections.get(day);
|
|
1660
|
-
if (!section) return;
|
|
1661
|
-
if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
|
|
1662
|
-
section.scrollIntoView({ block: "start" });
|
|
1663
|
-
this.strip?.setFocus(day);
|
|
1664
|
-
this.pinned = true;
|
|
1665
|
-
}
|
|
1666
|
-
fold = () => {
|
|
1667
|
-
this.pinned = false;
|
|
1668
|
-
if (this.strip?.expanded) this.strip.setExpanded(false);
|
|
1669
|
-
};
|
|
1670
|
-
onScroll = () => {
|
|
1671
|
-
if (this.frame !== null) return;
|
|
1672
|
-
this.frame = this.ctx.win.requestAnimationFrame(() => {
|
|
1673
|
-
this.frame = null;
|
|
1674
|
-
this.followTop();
|
|
1675
|
-
});
|
|
1676
|
-
};
|
|
1677
|
-
/** Marks the first day not hidden under the strip. */
|
|
1678
|
-
followTop() {
|
|
1679
|
-
if (!this.strip || this.pinned) return;
|
|
1680
|
-
const edge = this.strip.el.getBoundingClientRect().bottom + 1;
|
|
1681
|
-
for (const day of this.ctx.days) {
|
|
1682
|
-
const section = this.sections.get(day);
|
|
1683
|
-
if (section && section.getBoundingClientRect().bottom > edge) {
|
|
1684
|
-
this.strip.setFocus(day);
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
showSelection(_sel) {
|
|
1690
|
-
}
|
|
1691
|
-
hit(x, y) {
|
|
1692
|
-
const el = this.ctx.doc.elementFromPoint(x, y);
|
|
1693
|
-
const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
|
|
1694
|
-
this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
|
|
1695
|
-
if (stripDay !== null) return { kind: "day", day: stripDay };
|
|
1696
|
-
const section = el?.closest(".tnc-list-section");
|
|
1697
|
-
const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
|
|
1698
|
-
return day === null ? null : { kind: "day", day };
|
|
1699
|
-
}
|
|
1700
|
-
dragEnded() {
|
|
1701
|
-
this.strip?.markTarget(null);
|
|
1702
|
-
}
|
|
1703
|
-
getScroll() {
|
|
1704
|
-
return { top: this.scroller.scrollTop, left: 0 };
|
|
1705
|
-
}
|
|
1706
|
-
setScroll(s) {
|
|
1707
|
-
this.scroller.scrollTop = s.top;
|
|
1708
|
-
}
|
|
1709
|
-
elementsFor(id) {
|
|
1710
|
-
return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
|
|
1711
|
-
}
|
|
1712
|
-
destroy() {
|
|
1713
|
-
const { win } = this.ctx;
|
|
1714
|
-
win.removeEventListener("scroll", this.onScroll, { capture: true });
|
|
1715
|
-
if (this.frame !== null) win.cancelAnimationFrame(this.frame);
|
|
1716
|
-
this.loader?.disconnect();
|
|
1717
|
-
this.stripSize?.disconnect();
|
|
1718
|
-
this.strip?.destroy();
|
|
1719
|
-
for (const p of this.pools.values()) p.clear();
|
|
1720
|
-
this.el.remove();
|
|
1721
|
-
}
|
|
1722
|
-
};
|
|
1723
|
-
|
|
1724
1790
|
// src/dom/views/timegrid.ts
|
|
1725
1791
|
var BAR_GAP2 = 2;
|
|
1726
1792
|
var TimeGridView = class {
|
|
@@ -2926,6 +2992,9 @@ var Calendar = class {
|
|
|
2926
2992
|
slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
|
|
2927
2993
|
draggingId = null;
|
|
2928
2994
|
dragRow = null;
|
|
2995
|
+
strip = null;
|
|
2996
|
+
stripSize = null;
|
|
2997
|
+
rebuilding = false;
|
|
2929
2998
|
toolbarEl;
|
|
2930
2999
|
viewHost;
|
|
2931
3000
|
current = null;
|
|
@@ -3139,7 +3208,7 @@ var Calendar = class {
|
|
|
3139
3208
|
}
|
|
3140
3209
|
/** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
|
|
3141
3210
|
hitTest(clientX, clientY) {
|
|
3142
|
-
return this.
|
|
3211
|
+
return this.hitAt(clientX, clientY);
|
|
3143
3212
|
}
|
|
3144
3213
|
/** Item elements currently rendered for an id. */
|
|
3145
3214
|
elementsFor(id) {
|
|
@@ -3181,6 +3250,9 @@ var Calendar = class {
|
|
|
3181
3250
|
this.unbindEvents();
|
|
3182
3251
|
this.current?.destroy();
|
|
3183
3252
|
this.current = null;
|
|
3253
|
+
this.stripSize?.disconnect();
|
|
3254
|
+
this.strip?.destroy();
|
|
3255
|
+
this.strip = null;
|
|
3184
3256
|
this.root.remove();
|
|
3185
3257
|
}
|
|
3186
3258
|
/* ========================================================== view context */
|
|
@@ -3449,12 +3521,18 @@ var Calendar = class {
|
|
|
3449
3521
|
const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
|
|
3450
3522
|
this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
|
|
3451
3523
|
if (navigated) this.listEnd = null;
|
|
3452
|
-
const
|
|
3524
|
+
const prev = this.view;
|
|
3525
|
+
this.applyRange();
|
|
3453
3526
|
this.conflicts = dependencyConflicts(this.store);
|
|
3454
3527
|
this.root.className = "tnc";
|
|
3455
3528
|
this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
|
|
3456
3529
|
if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
|
|
3457
3530
|
this.renderToolbar();
|
|
3531
|
+
this.rebuilding = true;
|
|
3532
|
+
this.syncStrip(navigated);
|
|
3533
|
+
this.rebuilding = false;
|
|
3534
|
+
this.view.dataRange = this.dataRangeFor(this.view.range);
|
|
3535
|
+
const rangeChanged = rangeMoved(prev, this.view);
|
|
3458
3536
|
this.current?.destroy();
|
|
3459
3537
|
this.current = this.createView();
|
|
3460
3538
|
this.viewHost.replaceChildren(this.current.el);
|
|
@@ -3467,7 +3545,7 @@ var Calendar = class {
|
|
|
3467
3545
|
else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
|
|
3468
3546
|
if (rangeChanged) this.opts.onRangeChange?.(this.view);
|
|
3469
3547
|
}
|
|
3470
|
-
/** Sets the visible days and view info from the anchor
|
|
3548
|
+
/** Sets the visible days and view info from the anchor. */
|
|
3471
3549
|
applyRange() {
|
|
3472
3550
|
const { startDay, endDay } = this.computeRange();
|
|
3473
3551
|
const hidden = new Set(this.opts.hiddenDays);
|
|
@@ -3485,10 +3563,74 @@ var Calendar = class {
|
|
|
3485
3563
|
start: new Date(toInstant(startDay, 0, this.zone)),
|
|
3486
3564
|
end: new Date(toInstant(endDay, 0, this.zone))
|
|
3487
3565
|
};
|
|
3488
|
-
|
|
3489
|
-
this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
|
|
3566
|
+
this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
|
|
3490
3567
|
this.view.title = this.computeTitle(startDay, endDay);
|
|
3491
|
-
|
|
3568
|
+
}
|
|
3569
|
+
/** The range, run on to the end of the strip's days so their dots are right. */
|
|
3570
|
+
dataRangeFor(range) {
|
|
3571
|
+
const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
|
|
3572
|
+
if (end === range.endDay) return range;
|
|
3573
|
+
return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
|
|
3574
|
+
}
|
|
3575
|
+
/** Adds, updates or removes the date strip for the current view. */
|
|
3576
|
+
syncStrip(navigated) {
|
|
3577
|
+
const { view, listAgenda, dateStrip } = this.opts;
|
|
3578
|
+
const list = view === "list";
|
|
3579
|
+
const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
|
|
3580
|
+
if (!want) {
|
|
3581
|
+
if (!this.strip) return;
|
|
3582
|
+
this.stripSize?.disconnect();
|
|
3583
|
+
this.stripSize = null;
|
|
3584
|
+
this.strip.destroy();
|
|
3585
|
+
this.strip = null;
|
|
3586
|
+
this.root.style.removeProperty("--tnc-strip-h");
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
this.root.classList.add("tnc--has-strip");
|
|
3590
|
+
const { startDay, endDay } = this.view.range;
|
|
3591
|
+
if (!this.strip) {
|
|
3592
|
+
const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
|
|
3593
|
+
this.strip = strip;
|
|
3594
|
+
this.root.insertBefore(strip.el, this.viewHost);
|
|
3595
|
+
const RO = this.win.ResizeObserver;
|
|
3596
|
+
if (RO) {
|
|
3597
|
+
this.stripSize = new RO(() => strip.measure());
|
|
3598
|
+
this.stripSize.observe(strip.el);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
|
|
3602
|
+
else if (navigated) this.strip.setFocus(startDay, null);
|
|
3603
|
+
this.strip.draw();
|
|
3604
|
+
}
|
|
3605
|
+
pickStripDay(day, ev) {
|
|
3606
|
+
this.dateClick(day, ev);
|
|
3607
|
+
if (this.agendaList()) {
|
|
3608
|
+
this.showListDay(day);
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
const { startDay, endDay } = this.view.range;
|
|
3612
|
+
if (this.opts.view === "week" && day >= startDay && day < endDay) return;
|
|
3613
|
+
this.anchor = day;
|
|
3614
|
+
this.rebuild(true);
|
|
3615
|
+
}
|
|
3616
|
+
/** The strip moved to other days: load theirs too. */
|
|
3617
|
+
stripShown() {
|
|
3618
|
+
if (!this.strip || this.rebuilding || !this.view) return;
|
|
3619
|
+
const next = this.dataRangeFor(this.view.range);
|
|
3620
|
+
if (next.endDay === this.view.dataRange.endDay) return;
|
|
3621
|
+
this.view = { ...this.view, dataRange: next };
|
|
3622
|
+
this.opts.onRangeChange?.(this.view);
|
|
3623
|
+
}
|
|
3624
|
+
/** What is under a point, the date strip included. */
|
|
3625
|
+
hitAt(x, y) {
|
|
3626
|
+
const strip = this.strip;
|
|
3627
|
+
if (strip) {
|
|
3628
|
+
const el = this.doc.elementFromPoint(x, y);
|
|
3629
|
+
const day = el ? strip.dayAt(el) : null;
|
|
3630
|
+
strip.markTarget(day !== null && this.draggingId ? day : null);
|
|
3631
|
+
if (day !== null) return { kind: "day", day };
|
|
3632
|
+
}
|
|
3633
|
+
return this.current?.hit(x, y) ?? null;
|
|
3492
3634
|
}
|
|
3493
3635
|
agendaList() {
|
|
3494
3636
|
return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
|
|
@@ -3501,6 +3643,7 @@ var Calendar = class {
|
|
|
3501
3643
|
this.listEnd = end;
|
|
3502
3644
|
this.applyRange();
|
|
3503
3645
|
this.current?.render(null);
|
|
3646
|
+
this.strip?.draw();
|
|
3504
3647
|
this.opts.onRangeChange?.(this.view);
|
|
3505
3648
|
}
|
|
3506
3649
|
showListDay(day) {
|
|
@@ -3898,7 +4041,7 @@ var Calendar = class {
|
|
|
3898
4041
|
};
|
|
3899
4042
|
/** Proposal for the current pointer position. */
|
|
3900
4043
|
track(s, ev) {
|
|
3901
|
-
const hit = this.
|
|
4044
|
+
const hit = this.hitAt(ev.clientX, ev.clientY);
|
|
3902
4045
|
if (s.mode === "external") {
|
|
3903
4046
|
this.trackExternal(s, hit);
|
|
3904
4047
|
return;
|
|
@@ -4024,7 +4167,7 @@ var Calendar = class {
|
|
|
4024
4167
|
return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
|
|
4025
4168
|
}
|
|
4026
4169
|
if (hit.kind === "day") {
|
|
4027
|
-
const dd = hit.day - (grab.kind === "
|
|
4170
|
+
const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
|
|
4028
4171
|
if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
|
|
4029
4172
|
const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
|
|
4030
4173
|
const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
|
|
@@ -4159,7 +4302,7 @@ var Calendar = class {
|
|
|
4159
4302
|
this.draggingId = null;
|
|
4160
4303
|
this.dragRow = null;
|
|
4161
4304
|
if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
|
|
4162
|
-
this.
|
|
4305
|
+
this.strip?.markTarget(null);
|
|
4163
4306
|
if (!s.active) return;
|
|
4164
4307
|
if (s.mode === "select") {
|
|
4165
4308
|
if (cancel) this.unselectSilently();
|
|
@@ -4456,6 +4599,9 @@ function scrollerOf(el, axis) {
|
|
|
4456
4599
|
}
|
|
4457
4600
|
return doc.scrollingElement;
|
|
4458
4601
|
}
|
|
4602
|
+
function rangeMoved(prev, next) {
|
|
4603
|
+
return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
|
|
4604
|
+
}
|
|
4459
4605
|
|
|
4460
4606
|
// src/tasknotes/bases.ts
|
|
4461
4607
|
var FROM_BASES = {
|