@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/react.cjs
CHANGED
|
@@ -709,6 +709,9 @@ function defaultOptions() {
|
|
|
709
709
|
listDayCount: 7,
|
|
710
710
|
listStart: "date",
|
|
711
711
|
listAgenda: false,
|
|
712
|
+
dateStrip: false,
|
|
713
|
+
listLeadTitle: null,
|
|
714
|
+
listAddText: null,
|
|
712
715
|
monthDisplay: "full",
|
|
713
716
|
timeZone: "local",
|
|
714
717
|
locale: void 0,
|
|
@@ -759,6 +762,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
|
|
|
759
762
|
"listDayCount",
|
|
760
763
|
"listStart",
|
|
761
764
|
"listAgenda",
|
|
765
|
+
"dateStrip",
|
|
766
|
+
"listLeadTitle",
|
|
767
|
+
"listAddText",
|
|
762
768
|
"monthDisplay",
|
|
763
769
|
"timeZone",
|
|
764
770
|
"locale",
|
|
@@ -1248,12 +1254,244 @@ var DayGridView = class {
|
|
|
1248
1254
|
}
|
|
1249
1255
|
};
|
|
1250
1256
|
|
|
1257
|
+
// src/dom/views/list.ts
|
|
1258
|
+
var LOAD_AHEAD = "0px 0px 600px 0px";
|
|
1259
|
+
var ListView = class {
|
|
1260
|
+
constructor(ctx) {
|
|
1261
|
+
this.ctx = ctx;
|
|
1262
|
+
const { doc, win, opts } = ctx;
|
|
1263
|
+
this.el = h(doc, "div", "tnc-list");
|
|
1264
|
+
this.scroller = h(doc, "div", "tnc-list-scroll");
|
|
1265
|
+
this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
|
|
1266
|
+
this.scroller.append(this.body);
|
|
1267
|
+
this.el.append(this.scroller);
|
|
1268
|
+
this.agenda = opts.listAgenda;
|
|
1269
|
+
if (!this.agenda) return;
|
|
1270
|
+
this.el.classList.add("tnc-list--agenda");
|
|
1271
|
+
this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
|
|
1272
|
+
this.scroller.append(this.sentinel);
|
|
1273
|
+
const IO = win.IntersectionObserver;
|
|
1274
|
+
if (IO) {
|
|
1275
|
+
this.loader = new IO(
|
|
1276
|
+
(entries) => {
|
|
1277
|
+
if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
|
|
1278
|
+
},
|
|
1279
|
+
// Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
|
|
1280
|
+
{ root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
|
|
1284
|
+
this.scroller.addEventListener("wheel", this.fold, { passive: true });
|
|
1285
|
+
this.scroller.addEventListener("touchmove", this.fold, { passive: true });
|
|
1286
|
+
}
|
|
1287
|
+
ctx;
|
|
1288
|
+
el;
|
|
1289
|
+
scroller;
|
|
1290
|
+
body;
|
|
1291
|
+
agenda;
|
|
1292
|
+
pools = /* @__PURE__ */ new Map();
|
|
1293
|
+
sections = /* @__PURE__ */ new Map();
|
|
1294
|
+
lead = null;
|
|
1295
|
+
sentinel = null;
|
|
1296
|
+
loader = null;
|
|
1297
|
+
frame = null;
|
|
1298
|
+
/** A picked day stays marked until the list is scrolled by hand. */
|
|
1299
|
+
pinned = false;
|
|
1300
|
+
itemsOn(day) {
|
|
1301
|
+
const { store, opts } = this.ctx;
|
|
1302
|
+
const out = store.spanningIn(day, day + 1, true);
|
|
1303
|
+
const seen = new Set(out.map((n) => n.id));
|
|
1304
|
+
for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
|
|
1305
|
+
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));
|
|
1306
|
+
}
|
|
1307
|
+
render(_change) {
|
|
1308
|
+
const { ctx, agenda } = this;
|
|
1309
|
+
const { doc, days, opts } = ctx;
|
|
1310
|
+
const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
|
|
1311
|
+
const leadItems = [];
|
|
1312
|
+
const inLead = /* @__PURE__ */ new Set();
|
|
1313
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1314
|
+
let prev = null;
|
|
1315
|
+
for (const day of days) {
|
|
1316
|
+
let items = this.itemsOn(day);
|
|
1317
|
+
if (includes) {
|
|
1318
|
+
items = items.filter((n) => {
|
|
1319
|
+
if (!includes(n.item)) return true;
|
|
1320
|
+
if (!inLead.has(n.id)) {
|
|
1321
|
+
inLead.add(n.id);
|
|
1322
|
+
leadItems.push(n);
|
|
1323
|
+
}
|
|
1324
|
+
return false;
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if (!items.length && !agenda) continue;
|
|
1328
|
+
keep.add(day);
|
|
1329
|
+
let section = this.sections.get(day);
|
|
1330
|
+
let pool = this.pools.get(day);
|
|
1331
|
+
if (!section || !pool) {
|
|
1332
|
+
const arg = ctx.dayArg(day);
|
|
1333
|
+
const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
|
|
1334
|
+
const custom = opts.dayHeaderContent?.(arg);
|
|
1335
|
+
const label = custom ?? ctx.fmt.longDate(day);
|
|
1336
|
+
heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
|
|
1337
|
+
const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1338
|
+
section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
|
|
1339
|
+
if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
|
|
1340
|
+
opts.dayHeaderDidMount?.({ ...arg, el: heading });
|
|
1341
|
+
pool = new ItemPool(ctx, rows);
|
|
1342
|
+
this.sections.set(day, section);
|
|
1343
|
+
this.pools.set(day, pool);
|
|
1344
|
+
}
|
|
1345
|
+
section.classList.toggle("is-empty", !items.length);
|
|
1346
|
+
this.fill(pool, items, () => day);
|
|
1347
|
+
if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
|
|
1348
|
+
if (prev) prev.after(section);
|
|
1349
|
+
else this.body.prepend(section);
|
|
1350
|
+
}
|
|
1351
|
+
prev = section;
|
|
1352
|
+
}
|
|
1353
|
+
for (const [day, section] of this.sections) {
|
|
1354
|
+
if (keep.has(day)) continue;
|
|
1355
|
+
this.pools.get(day)?.clear();
|
|
1356
|
+
this.pools.delete(day);
|
|
1357
|
+
section.remove();
|
|
1358
|
+
this.sections.delete(day);
|
|
1359
|
+
}
|
|
1360
|
+
this.renderLead(leadItems);
|
|
1361
|
+
let empty = this.body.querySelector(":scope > .tnc-list-empty");
|
|
1362
|
+
if (!keep.size && !leadItems.length) {
|
|
1363
|
+
if (!empty) {
|
|
1364
|
+
empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
|
|
1365
|
+
this.body.append(empty);
|
|
1366
|
+
}
|
|
1367
|
+
} else empty?.remove();
|
|
1368
|
+
this.ctx.strip?.draw();
|
|
1369
|
+
if (this.loader && this.sentinel) {
|
|
1370
|
+
this.loader.unobserve(this.sentinel);
|
|
1371
|
+
this.loader.observe(this.sentinel);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
/** Places items in a pool, keeping DOM order equal to item order. */
|
|
1375
|
+
fill(pool, items, dayFor) {
|
|
1376
|
+
pool.begin();
|
|
1377
|
+
for (const n of items) {
|
|
1378
|
+
const d = dayFor(n);
|
|
1379
|
+
pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
|
|
1380
|
+
}
|
|
1381
|
+
pool.end();
|
|
1382
|
+
for (const n of items) {
|
|
1383
|
+
const el = pool.elementsFor(n.id)[0];
|
|
1384
|
+
if (el) pool.container.append(el);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
renderLead(items) {
|
|
1388
|
+
const { ctx } = this;
|
|
1389
|
+
const { doc, opts } = ctx;
|
|
1390
|
+
if (!items.length || opts.listLeadTitle === null) {
|
|
1391
|
+
if (!this.lead) return;
|
|
1392
|
+
opts.listLeadWillUnmount?.({ el: this.lead.slot });
|
|
1393
|
+
this.lead.pool.clear();
|
|
1394
|
+
this.lead.section.remove();
|
|
1395
|
+
this.lead = null;
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (!this.lead) {
|
|
1399
|
+
const title = opts.listLeadTitle;
|
|
1400
|
+
const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
|
|
1401
|
+
const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
|
|
1402
|
+
const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1403
|
+
const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
|
|
1404
|
+
this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
|
|
1405
|
+
opts.listLeadDidMount?.({ el: slot });
|
|
1406
|
+
}
|
|
1407
|
+
if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
|
|
1408
|
+
const first = ctx.days[0] ?? ctx.view.range.startDay;
|
|
1409
|
+
this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
|
|
1410
|
+
}
|
|
1411
|
+
addRow(day) {
|
|
1412
|
+
const { ctx } = this;
|
|
1413
|
+
const label = ctx.opts.listAddText ?? "";
|
|
1414
|
+
const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
|
|
1415
|
+
btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
|
|
1416
|
+
return btn;
|
|
1417
|
+
}
|
|
1418
|
+
/** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
|
|
1419
|
+
scrollToDay(day) {
|
|
1420
|
+
const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
|
|
1421
|
+
if (!section) return;
|
|
1422
|
+
this.ctx.strip?.measure();
|
|
1423
|
+
section.scrollIntoView({ block: "start" });
|
|
1424
|
+
this.ctx.strip?.setFocus(day);
|
|
1425
|
+
this.pinned = true;
|
|
1426
|
+
}
|
|
1427
|
+
fold = () => {
|
|
1428
|
+
this.pinned = false;
|
|
1429
|
+
const strip = this.ctx.strip;
|
|
1430
|
+
if (strip?.expanded) strip.setExpanded(false);
|
|
1431
|
+
};
|
|
1432
|
+
onScroll = () => {
|
|
1433
|
+
if (this.frame !== null) return;
|
|
1434
|
+
this.frame = this.ctx.win.requestAnimationFrame(() => {
|
|
1435
|
+
this.frame = null;
|
|
1436
|
+
this.followTop();
|
|
1437
|
+
});
|
|
1438
|
+
};
|
|
1439
|
+
/** Marks the first day not hidden under the strip. */
|
|
1440
|
+
followTop() {
|
|
1441
|
+
const strip = this.ctx.strip;
|
|
1442
|
+
if (!strip || this.pinned) return;
|
|
1443
|
+
const edge = strip.el.getBoundingClientRect().bottom + 1;
|
|
1444
|
+
for (const day of this.ctx.days) {
|
|
1445
|
+
const section = this.sections.get(day);
|
|
1446
|
+
if (section && section.getBoundingClientRect().bottom > edge) {
|
|
1447
|
+
strip.setFocus(day);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
showSelection(_sel) {
|
|
1453
|
+
}
|
|
1454
|
+
hit(x, y) {
|
|
1455
|
+
const el = this.ctx.doc.elementFromPoint(x, y);
|
|
1456
|
+
const section = el?.closest(".tnc-list-section");
|
|
1457
|
+
if (!section || !this.body.contains(section)) return null;
|
|
1458
|
+
if (section === this.lead?.section) {
|
|
1459
|
+
const item = el?.closest(".tnc-item");
|
|
1460
|
+
const meta = item ? this.ctx.meta.get(item) : void 0;
|
|
1461
|
+
return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
|
|
1462
|
+
}
|
|
1463
|
+
const day = parseDay(section.dataset.date ?? "");
|
|
1464
|
+
return day === null ? null : { kind: "day", day };
|
|
1465
|
+
}
|
|
1466
|
+
getScroll() {
|
|
1467
|
+
return { top: this.scroller.scrollTop, left: 0 };
|
|
1468
|
+
}
|
|
1469
|
+
setScroll(s) {
|
|
1470
|
+
this.scroller.scrollTop = s.top;
|
|
1471
|
+
}
|
|
1472
|
+
elementsFor(id) {
|
|
1473
|
+
const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
|
|
1474
|
+
return pools.flatMap((p) => p.elementsFor(id));
|
|
1475
|
+
}
|
|
1476
|
+
destroy() {
|
|
1477
|
+
const { win, opts } = this.ctx;
|
|
1478
|
+
win.removeEventListener("scroll", this.onScroll, { capture: true });
|
|
1479
|
+
if (this.frame !== null) win.cancelAnimationFrame(this.frame);
|
|
1480
|
+
this.loader?.disconnect();
|
|
1481
|
+
if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
|
|
1482
|
+
this.lead?.pool.clear();
|
|
1483
|
+
for (const p of this.pools.values()) p.clear();
|
|
1484
|
+
this.el.remove();
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1251
1488
|
// src/dom/views/strip.ts
|
|
1252
1489
|
var SWIPE_PX = 40;
|
|
1253
1490
|
var DateStrip = class {
|
|
1254
|
-
constructor(ctx, pick, focus) {
|
|
1491
|
+
constructor(ctx, pick, focus, onShown) {
|
|
1255
1492
|
this.ctx = ctx;
|
|
1256
1493
|
this.pick = pick;
|
|
1494
|
+
this.onShown = onShown;
|
|
1257
1495
|
const { doc, text } = ctx;
|
|
1258
1496
|
this.focus = focus;
|
|
1259
1497
|
this.shown = startOfWeek(focus, ctx.firstDay);
|
|
@@ -1265,8 +1503,10 @@ var DateStrip = class {
|
|
|
1265
1503
|
const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
|
|
1266
1504
|
prev.addEventListener("click", () => this.step(-1));
|
|
1267
1505
|
next.addEventListener("click", () => this.step(1));
|
|
1506
|
+
this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
|
|
1507
|
+
this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
|
|
1268
1508
|
this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
|
|
1269
|
-
head.append(this.title, prev, next, this.slot);
|
|
1509
|
+
head.append(this.title, this.todayBtn, prev, next, this.slot);
|
|
1270
1510
|
this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
|
|
1271
1511
|
this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
|
|
1272
1512
|
const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
|
|
@@ -1283,27 +1523,45 @@ var DateStrip = class {
|
|
|
1283
1523
|
}
|
|
1284
1524
|
ctx;
|
|
1285
1525
|
pick;
|
|
1526
|
+
onShown;
|
|
1286
1527
|
el;
|
|
1287
1528
|
title;
|
|
1288
1529
|
dows;
|
|
1289
1530
|
grid;
|
|
1290
1531
|
slot;
|
|
1532
|
+
todayBtn;
|
|
1291
1533
|
/** Day at the top of the list. */
|
|
1292
1534
|
focus;
|
|
1293
1535
|
/** First day of the week shown, or the month shown when expanded. */
|
|
1294
1536
|
shown;
|
|
1295
1537
|
target = null;
|
|
1538
|
+
/** Days tinted as on screen besides the focus (a time grid's range). */
|
|
1539
|
+
range = null;
|
|
1540
|
+
shownKey = "";
|
|
1296
1541
|
swipe = null;
|
|
1297
1542
|
swallowClick = false;
|
|
1298
1543
|
expanded = false;
|
|
1299
|
-
/** The list scrolled: mark
|
|
1300
|
-
setFocus(day) {
|
|
1301
|
-
|
|
1302
|
-
this.focus
|
|
1303
|
-
|
|
1304
|
-
|
|
1544
|
+
/** The list scrolled or the grid moved: mark the day and keep it in view. */
|
|
1545
|
+
setFocus(day, range = this.range) {
|
|
1546
|
+
const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
|
|
1547
|
+
if (day === this.focus && sameRange) return;
|
|
1548
|
+
this.range = range;
|
|
1549
|
+
if (day !== this.focus) {
|
|
1550
|
+
this.focus = day;
|
|
1551
|
+
if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
|
|
1552
|
+
else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
|
|
1553
|
+
}
|
|
1305
1554
|
this.draw();
|
|
1306
1555
|
}
|
|
1556
|
+
/** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
|
|
1557
|
+
measure() {
|
|
1558
|
+
this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
|
|
1559
|
+
}
|
|
1560
|
+
/** First day and end (exclusive) of the days on show. */
|
|
1561
|
+
window() {
|
|
1562
|
+
const days = this.days();
|
|
1563
|
+
return { start: days[0], end: days[days.length - 1] + 1 };
|
|
1564
|
+
}
|
|
1307
1565
|
setExpanded(on) {
|
|
1308
1566
|
if (on === this.expanded) return;
|
|
1309
1567
|
this.expanded = on;
|
|
@@ -1341,6 +1599,7 @@ var DateStrip = class {
|
|
|
1341
1599
|
this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
|
|
1342
1600
|
this.title.setAttribute("aria-expanded", String(this.expanded));
|
|
1343
1601
|
this.el.classList.toggle("is-expanded", this.expanded);
|
|
1602
|
+
this.todayBtn.hidden = this.focus === ctx.today;
|
|
1344
1603
|
this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
|
|
1345
1604
|
const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
|
|
1346
1605
|
const month = ymdOfDay(this.shown).month;
|
|
@@ -1357,6 +1616,7 @@ var DateStrip = class {
|
|
|
1357
1616
|
"tnc-strip-day",
|
|
1358
1617
|
...ctx.dayClasses(d),
|
|
1359
1618
|
d === this.focus ? "is-focus" : "",
|
|
1619
|
+
d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
|
|
1360
1620
|
count ? "has-items" : "",
|
|
1361
1621
|
this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
|
|
1362
1622
|
d === this.target ? "is-drop-target" : ""
|
|
@@ -1376,6 +1636,11 @@ var DateStrip = class {
|
|
|
1376
1636
|
this.grid.replaceChildren(...cells);
|
|
1377
1637
|
this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
|
|
1378
1638
|
if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
|
|
1639
|
+
const key = `${days[0]}:${days.length}`;
|
|
1640
|
+
if (key !== this.shownKey) {
|
|
1641
|
+
this.shownKey = key;
|
|
1642
|
+
this.onShown();
|
|
1643
|
+
}
|
|
1379
1644
|
}
|
|
1380
1645
|
onClick = (ev) => {
|
|
1381
1646
|
const day = this.dayAt(ev.target);
|
|
@@ -1430,205 +1695,6 @@ var DateStrip = class {
|
|
|
1430
1695
|
}
|
|
1431
1696
|
};
|
|
1432
1697
|
|
|
1433
|
-
// src/dom/views/list.ts
|
|
1434
|
-
var LOAD_AHEAD = "0px 0px 600px 0px";
|
|
1435
|
-
var ListView = class {
|
|
1436
|
-
constructor(ctx) {
|
|
1437
|
-
this.ctx = ctx;
|
|
1438
|
-
const { doc, win, opts } = ctx;
|
|
1439
|
-
this.el = h(doc, "div", "tnc-list");
|
|
1440
|
-
this.scroller = h(doc, "div", "tnc-list-scroll");
|
|
1441
|
-
this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
|
|
1442
|
-
this.scroller.append(this.body);
|
|
1443
|
-
this.el.append(this.scroller);
|
|
1444
|
-
if (!opts.listAgenda) return;
|
|
1445
|
-
this.el.classList.add("tnc-list--agenda");
|
|
1446
|
-
this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
|
|
1447
|
-
this.el.prepend(this.strip.el);
|
|
1448
|
-
this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
|
|
1449
|
-
this.scroller.append(this.sentinel);
|
|
1450
|
-
const globals = win;
|
|
1451
|
-
const IO = globals.IntersectionObserver;
|
|
1452
|
-
if (IO) {
|
|
1453
|
-
this.loader = new IO(
|
|
1454
|
-
(entries) => {
|
|
1455
|
-
if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
|
|
1456
|
-
},
|
|
1457
|
-
// Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
|
|
1458
|
-
{ root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
const RO = globals.ResizeObserver;
|
|
1462
|
-
if (RO) {
|
|
1463
|
-
const strip = this.strip.el;
|
|
1464
|
-
this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
|
|
1465
|
-
this.stripSize.observe(strip);
|
|
1466
|
-
}
|
|
1467
|
-
win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
|
|
1468
|
-
this.scroller.addEventListener("wheel", this.fold, { passive: true });
|
|
1469
|
-
this.scroller.addEventListener("touchmove", this.fold, { passive: true });
|
|
1470
|
-
}
|
|
1471
|
-
ctx;
|
|
1472
|
-
el;
|
|
1473
|
-
scroller;
|
|
1474
|
-
body;
|
|
1475
|
-
pools = /* @__PURE__ */ new Map();
|
|
1476
|
-
sections = /* @__PURE__ */ new Map();
|
|
1477
|
-
strip = null;
|
|
1478
|
-
sentinel = null;
|
|
1479
|
-
loader = null;
|
|
1480
|
-
stripSize = null;
|
|
1481
|
-
frame = null;
|
|
1482
|
-
/** A picked day stays marked until the list is scrolled by hand. */
|
|
1483
|
-
pinned = false;
|
|
1484
|
-
itemsOn(day) {
|
|
1485
|
-
const { store, opts } = this.ctx;
|
|
1486
|
-
const out = store.spanningIn(day, day + 1, true);
|
|
1487
|
-
const seen = new Set(out.map((n) => n.id));
|
|
1488
|
-
for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
|
|
1489
|
-
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));
|
|
1490
|
-
}
|
|
1491
|
-
render(_change) {
|
|
1492
|
-
const { ctx } = this;
|
|
1493
|
-
const { doc, days } = ctx;
|
|
1494
|
-
const agenda = !!this.strip;
|
|
1495
|
-
const keep = /* @__PURE__ */ new Set();
|
|
1496
|
-
let prev = null;
|
|
1497
|
-
for (const day of days) {
|
|
1498
|
-
const items = this.itemsOn(day);
|
|
1499
|
-
if (!items.length && !agenda) continue;
|
|
1500
|
-
keep.add(day);
|
|
1501
|
-
let section = this.sections.get(day);
|
|
1502
|
-
let pool = this.pools.get(day);
|
|
1503
|
-
if (!section || !pool) {
|
|
1504
|
-
const arg = ctx.dayArg(day);
|
|
1505
|
-
const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
|
|
1506
|
-
const custom = ctx.opts.dayHeaderContent?.(arg);
|
|
1507
|
-
const label = custom ?? ctx.fmt.longDate(day);
|
|
1508
|
-
heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
|
|
1509
|
-
const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
|
|
1510
|
-
section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
|
|
1511
|
-
ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
|
|
1512
|
-
pool = new ItemPool(ctx, rows2);
|
|
1513
|
-
this.sections.set(day, section);
|
|
1514
|
-
this.pools.set(day, pool);
|
|
1515
|
-
}
|
|
1516
|
-
section.classList.toggle("is-empty", !items.length);
|
|
1517
|
-
pool.begin();
|
|
1518
|
-
for (const n of items) {
|
|
1519
|
-
pool.place(n.id, {
|
|
1520
|
-
n,
|
|
1521
|
-
day,
|
|
1522
|
-
isStart: n.startDay === day,
|
|
1523
|
-
isEnd: n.endDay === day + 1,
|
|
1524
|
-
variant: "list",
|
|
1525
|
-
mirror: false
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
pool.end();
|
|
1529
|
-
const rows = pool.container;
|
|
1530
|
-
for (const n of items) {
|
|
1531
|
-
const el = pool.elementsFor(n.id)[0];
|
|
1532
|
-
if (el) rows.append(el);
|
|
1533
|
-
}
|
|
1534
|
-
if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
|
|
1535
|
-
if (prev) prev.after(section);
|
|
1536
|
-
else this.body.prepend(section);
|
|
1537
|
-
}
|
|
1538
|
-
prev = section;
|
|
1539
|
-
}
|
|
1540
|
-
for (const [day, section] of this.sections) {
|
|
1541
|
-
if (keep.has(day)) continue;
|
|
1542
|
-
this.pools.get(day)?.clear();
|
|
1543
|
-
this.pools.delete(day);
|
|
1544
|
-
section.remove();
|
|
1545
|
-
this.sections.delete(day);
|
|
1546
|
-
}
|
|
1547
|
-
let empty = this.body.querySelector(":scope > .tnc-list-empty");
|
|
1548
|
-
if (!keep.size) {
|
|
1549
|
-
if (!empty) {
|
|
1550
|
-
empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
|
|
1551
|
-
this.body.append(empty);
|
|
1552
|
-
}
|
|
1553
|
-
} else empty?.remove();
|
|
1554
|
-
this.strip?.draw();
|
|
1555
|
-
if (this.loader && this.sentinel) {
|
|
1556
|
-
this.loader.unobserve(this.sentinel);
|
|
1557
|
-
this.loader.observe(this.sentinel);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
/** A strip day: the host hears of it, and the list brings it to the top. */
|
|
1561
|
-
pick(day, ev) {
|
|
1562
|
-
this.ctx.dateClick(day, ev);
|
|
1563
|
-
this.ctx.showListDay(day);
|
|
1564
|
-
}
|
|
1565
|
-
/** Brings a day's section to the top, under the strip. */
|
|
1566
|
-
scrollToDay(day) {
|
|
1567
|
-
const section = this.sections.get(day);
|
|
1568
|
-
if (!section) return;
|
|
1569
|
-
if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
|
|
1570
|
-
section.scrollIntoView({ block: "start" });
|
|
1571
|
-
this.strip?.setFocus(day);
|
|
1572
|
-
this.pinned = true;
|
|
1573
|
-
}
|
|
1574
|
-
fold = () => {
|
|
1575
|
-
this.pinned = false;
|
|
1576
|
-
if (this.strip?.expanded) this.strip.setExpanded(false);
|
|
1577
|
-
};
|
|
1578
|
-
onScroll = () => {
|
|
1579
|
-
if (this.frame !== null) return;
|
|
1580
|
-
this.frame = this.ctx.win.requestAnimationFrame(() => {
|
|
1581
|
-
this.frame = null;
|
|
1582
|
-
this.followTop();
|
|
1583
|
-
});
|
|
1584
|
-
};
|
|
1585
|
-
/** Marks the first day not hidden under the strip. */
|
|
1586
|
-
followTop() {
|
|
1587
|
-
if (!this.strip || this.pinned) return;
|
|
1588
|
-
const edge = this.strip.el.getBoundingClientRect().bottom + 1;
|
|
1589
|
-
for (const day of this.ctx.days) {
|
|
1590
|
-
const section = this.sections.get(day);
|
|
1591
|
-
if (section && section.getBoundingClientRect().bottom > edge) {
|
|
1592
|
-
this.strip.setFocus(day);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
showSelection(_sel) {
|
|
1598
|
-
}
|
|
1599
|
-
hit(x, y) {
|
|
1600
|
-
const el = this.ctx.doc.elementFromPoint(x, y);
|
|
1601
|
-
const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
|
|
1602
|
-
this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
|
|
1603
|
-
if (stripDay !== null) return { kind: "day", day: stripDay };
|
|
1604
|
-
const section = el?.closest(".tnc-list-section");
|
|
1605
|
-
const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
|
|
1606
|
-
return day === null ? null : { kind: "day", day };
|
|
1607
|
-
}
|
|
1608
|
-
dragEnded() {
|
|
1609
|
-
this.strip?.markTarget(null);
|
|
1610
|
-
}
|
|
1611
|
-
getScroll() {
|
|
1612
|
-
return { top: this.scroller.scrollTop, left: 0 };
|
|
1613
|
-
}
|
|
1614
|
-
setScroll(s) {
|
|
1615
|
-
this.scroller.scrollTop = s.top;
|
|
1616
|
-
}
|
|
1617
|
-
elementsFor(id) {
|
|
1618
|
-
return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
|
|
1619
|
-
}
|
|
1620
|
-
destroy() {
|
|
1621
|
-
const { win } = this.ctx;
|
|
1622
|
-
win.removeEventListener("scroll", this.onScroll, { capture: true });
|
|
1623
|
-
if (this.frame !== null) win.cancelAnimationFrame(this.frame);
|
|
1624
|
-
this.loader?.disconnect();
|
|
1625
|
-
this.stripSize?.disconnect();
|
|
1626
|
-
this.strip?.destroy();
|
|
1627
|
-
for (const p of this.pools.values()) p.clear();
|
|
1628
|
-
this.el.remove();
|
|
1629
|
-
}
|
|
1630
|
-
};
|
|
1631
|
-
|
|
1632
1698
|
// src/dom/views/timegrid.ts
|
|
1633
1699
|
var BAR_GAP2 = 2;
|
|
1634
1700
|
var TimeGridView = class {
|
|
@@ -2834,6 +2900,9 @@ var Calendar = class {
|
|
|
2834
2900
|
slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
|
|
2835
2901
|
draggingId = null;
|
|
2836
2902
|
dragRow = null;
|
|
2903
|
+
strip = null;
|
|
2904
|
+
stripSize = null;
|
|
2905
|
+
rebuilding = false;
|
|
2837
2906
|
toolbarEl;
|
|
2838
2907
|
viewHost;
|
|
2839
2908
|
current = null;
|
|
@@ -3047,7 +3116,7 @@ var Calendar = class {
|
|
|
3047
3116
|
}
|
|
3048
3117
|
/** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
|
|
3049
3118
|
hitTest(clientX, clientY) {
|
|
3050
|
-
return this.
|
|
3119
|
+
return this.hitAt(clientX, clientY);
|
|
3051
3120
|
}
|
|
3052
3121
|
/** Item elements currently rendered for an id. */
|
|
3053
3122
|
elementsFor(id) {
|
|
@@ -3089,6 +3158,9 @@ var Calendar = class {
|
|
|
3089
3158
|
this.unbindEvents();
|
|
3090
3159
|
this.current?.destroy();
|
|
3091
3160
|
this.current = null;
|
|
3161
|
+
this.stripSize?.disconnect();
|
|
3162
|
+
this.strip?.destroy();
|
|
3163
|
+
this.strip = null;
|
|
3092
3164
|
this.root.remove();
|
|
3093
3165
|
}
|
|
3094
3166
|
/* ========================================================== view context */
|
|
@@ -3357,12 +3429,18 @@ var Calendar = class {
|
|
|
3357
3429
|
const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
|
|
3358
3430
|
this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
|
|
3359
3431
|
if (navigated) this.listEnd = null;
|
|
3360
|
-
const
|
|
3432
|
+
const prev = this.view;
|
|
3433
|
+
this.applyRange();
|
|
3361
3434
|
this.conflicts = dependencyConflicts(this.store);
|
|
3362
3435
|
this.root.className = "tnc";
|
|
3363
3436
|
this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
|
|
3364
3437
|
if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
|
|
3365
3438
|
this.renderToolbar();
|
|
3439
|
+
this.rebuilding = true;
|
|
3440
|
+
this.syncStrip(navigated);
|
|
3441
|
+
this.rebuilding = false;
|
|
3442
|
+
this.view.dataRange = this.dataRangeFor(this.view.range);
|
|
3443
|
+
const rangeChanged = rangeMoved(prev, this.view);
|
|
3366
3444
|
this.current?.destroy();
|
|
3367
3445
|
this.current = this.createView();
|
|
3368
3446
|
this.viewHost.replaceChildren(this.current.el);
|
|
@@ -3375,7 +3453,7 @@ var Calendar = class {
|
|
|
3375
3453
|
else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
|
|
3376
3454
|
if (rangeChanged) this.opts.onRangeChange?.(this.view);
|
|
3377
3455
|
}
|
|
3378
|
-
/** Sets the visible days and view info from the anchor
|
|
3456
|
+
/** Sets the visible days and view info from the anchor. */
|
|
3379
3457
|
applyRange() {
|
|
3380
3458
|
const { startDay, endDay } = this.computeRange();
|
|
3381
3459
|
const hidden = new Set(this.opts.hiddenDays);
|
|
@@ -3393,10 +3471,74 @@ var Calendar = class {
|
|
|
3393
3471
|
start: new Date(toInstant(startDay, 0, this.zone)),
|
|
3394
3472
|
end: new Date(toInstant(endDay, 0, this.zone))
|
|
3395
3473
|
};
|
|
3396
|
-
|
|
3397
|
-
this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
|
|
3474
|
+
this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
|
|
3398
3475
|
this.view.title = this.computeTitle(startDay, endDay);
|
|
3399
|
-
|
|
3476
|
+
}
|
|
3477
|
+
/** The range, run on to the end of the strip's days so their dots are right. */
|
|
3478
|
+
dataRangeFor(range) {
|
|
3479
|
+
const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
|
|
3480
|
+
if (end === range.endDay) return range;
|
|
3481
|
+
return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
|
|
3482
|
+
}
|
|
3483
|
+
/** Adds, updates or removes the date strip for the current view. */
|
|
3484
|
+
syncStrip(navigated) {
|
|
3485
|
+
const { view, listAgenda, dateStrip } = this.opts;
|
|
3486
|
+
const list = view === "list";
|
|
3487
|
+
const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
|
|
3488
|
+
if (!want) {
|
|
3489
|
+
if (!this.strip) return;
|
|
3490
|
+
this.stripSize?.disconnect();
|
|
3491
|
+
this.stripSize = null;
|
|
3492
|
+
this.strip.destroy();
|
|
3493
|
+
this.strip = null;
|
|
3494
|
+
this.root.style.removeProperty("--tnc-strip-h");
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
this.root.classList.add("tnc--has-strip");
|
|
3498
|
+
const { startDay, endDay } = this.view.range;
|
|
3499
|
+
if (!this.strip) {
|
|
3500
|
+
const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
|
|
3501
|
+
this.strip = strip;
|
|
3502
|
+
this.root.insertBefore(strip.el, this.viewHost);
|
|
3503
|
+
const RO = this.win.ResizeObserver;
|
|
3504
|
+
if (RO) {
|
|
3505
|
+
this.stripSize = new RO(() => strip.measure());
|
|
3506
|
+
this.stripSize.observe(strip.el);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
|
|
3510
|
+
else if (navigated) this.strip.setFocus(startDay, null);
|
|
3511
|
+
this.strip.draw();
|
|
3512
|
+
}
|
|
3513
|
+
pickStripDay(day, ev) {
|
|
3514
|
+
this.dateClick(day, ev);
|
|
3515
|
+
if (this.agendaList()) {
|
|
3516
|
+
this.showListDay(day);
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
const { startDay, endDay } = this.view.range;
|
|
3520
|
+
if (this.opts.view === "week" && day >= startDay && day < endDay) return;
|
|
3521
|
+
this.anchor = day;
|
|
3522
|
+
this.rebuild(true);
|
|
3523
|
+
}
|
|
3524
|
+
/** The strip moved to other days: load theirs too. */
|
|
3525
|
+
stripShown() {
|
|
3526
|
+
if (!this.strip || this.rebuilding || !this.view) return;
|
|
3527
|
+
const next = this.dataRangeFor(this.view.range);
|
|
3528
|
+
if (next.endDay === this.view.dataRange.endDay) return;
|
|
3529
|
+
this.view = { ...this.view, dataRange: next };
|
|
3530
|
+
this.opts.onRangeChange?.(this.view);
|
|
3531
|
+
}
|
|
3532
|
+
/** What is under a point, the date strip included. */
|
|
3533
|
+
hitAt(x, y) {
|
|
3534
|
+
const strip = this.strip;
|
|
3535
|
+
if (strip) {
|
|
3536
|
+
const el = this.doc.elementFromPoint(x, y);
|
|
3537
|
+
const day = el ? strip.dayAt(el) : null;
|
|
3538
|
+
strip.markTarget(day !== null && this.draggingId ? day : null);
|
|
3539
|
+
if (day !== null) return { kind: "day", day };
|
|
3540
|
+
}
|
|
3541
|
+
return this.current?.hit(x, y) ?? null;
|
|
3400
3542
|
}
|
|
3401
3543
|
agendaList() {
|
|
3402
3544
|
return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
|
|
@@ -3409,6 +3551,7 @@ var Calendar = class {
|
|
|
3409
3551
|
this.listEnd = end;
|
|
3410
3552
|
this.applyRange();
|
|
3411
3553
|
this.current?.render(null);
|
|
3554
|
+
this.strip?.draw();
|
|
3412
3555
|
this.opts.onRangeChange?.(this.view);
|
|
3413
3556
|
}
|
|
3414
3557
|
showListDay(day) {
|
|
@@ -3806,7 +3949,7 @@ var Calendar = class {
|
|
|
3806
3949
|
};
|
|
3807
3950
|
/** Proposal for the current pointer position. */
|
|
3808
3951
|
track(s, ev) {
|
|
3809
|
-
const hit = this.
|
|
3952
|
+
const hit = this.hitAt(ev.clientX, ev.clientY);
|
|
3810
3953
|
if (s.mode === "external") {
|
|
3811
3954
|
this.trackExternal(s, hit);
|
|
3812
3955
|
return;
|
|
@@ -3932,7 +4075,7 @@ var Calendar = class {
|
|
|
3932
4075
|
return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
|
|
3933
4076
|
}
|
|
3934
4077
|
if (hit.kind === "day") {
|
|
3935
|
-
const dd = hit.day - (grab.kind === "
|
|
4078
|
+
const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
|
|
3936
4079
|
if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
|
|
3937
4080
|
const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
|
|
3938
4081
|
const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
|
|
@@ -4067,7 +4210,7 @@ var Calendar = class {
|
|
|
4067
4210
|
this.draggingId = null;
|
|
4068
4211
|
this.dragRow = null;
|
|
4069
4212
|
if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
|
|
4070
|
-
this.
|
|
4213
|
+
this.strip?.markTarget(null);
|
|
4071
4214
|
if (!s.active) return;
|
|
4072
4215
|
if (s.mode === "select") {
|
|
4073
4216
|
if (cancel) this.unselectSilently();
|
|
@@ -4364,6 +4507,9 @@ function scrollerOf(el, axis) {
|
|
|
4364
4507
|
}
|
|
4365
4508
|
return doc.scrollingElement;
|
|
4366
4509
|
}
|
|
4510
|
+
function rangeMoved(prev, next) {
|
|
4511
|
+
return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
|
|
4512
|
+
}
|
|
4367
4513
|
|
|
4368
4514
|
// src/react/index.tsx
|
|
4369
4515
|
var import_jsx_runtime = require("react/jsx-runtime");
|