@orkestrel/form 0.0.8 → 0.0.10
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/README.md +1 -1
- package/dist/src/browser/index.d.ts +726 -0
- package/dist/src/browser/index.js +916 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +183 -17
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +95 -15
- package/dist/src/core/index.d.ts +95 -15
- package/dist/src/core/index.js +183 -17
- package/dist/src/core/index.js.map +1 -1
- package/package.json +31 -18
package/dist/src/core/index.js
CHANGED
|
@@ -1268,6 +1268,11 @@ var Form = class {
|
|
|
1268
1268
|
#touched = /* @__PURE__ */ new Set();
|
|
1269
1269
|
#disabled = /* @__PURE__ */ new Map();
|
|
1270
1270
|
#invalidations = /* @__PURE__ */ new Map();
|
|
1271
|
+
#revisions = /* @__PURE__ */ new Map();
|
|
1272
|
+
#captured = /* @__PURE__ */ new Map();
|
|
1273
|
+
#submission;
|
|
1274
|
+
#controller;
|
|
1275
|
+
#preparing;
|
|
1271
1276
|
#resolvers = Promise.withResolvers();
|
|
1272
1277
|
#errors = Object.freeze([]);
|
|
1273
1278
|
#status = "editing";
|
|
@@ -1322,10 +1327,14 @@ var Form = class {
|
|
|
1322
1327
|
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1323
1328
|
return Object.freeze(values);
|
|
1324
1329
|
}
|
|
1325
|
-
/** Holds the answers the
|
|
1330
|
+
/** Holds the acknowledged answers, initially the schema's defaults overlaid with seeded values. */
|
|
1326
1331
|
get baseline() {
|
|
1327
1332
|
return this.#baseline;
|
|
1328
1333
|
}
|
|
1334
|
+
/** Holds the submission awaiting acknowledgement, or `undefined` when none is pending. */
|
|
1335
|
+
get pending() {
|
|
1336
|
+
return this.#submission;
|
|
1337
|
+
}
|
|
1329
1338
|
/** Holds every error the last completed evaluation produced. */
|
|
1330
1339
|
get errors() {
|
|
1331
1340
|
return this.#errors;
|
|
@@ -1348,7 +1357,7 @@ var Form = class {
|
|
|
1348
1357
|
get valid() {
|
|
1349
1358
|
return this.#errors.length === 0;
|
|
1350
1359
|
}
|
|
1351
|
-
/** Reports whether any answer
|
|
1360
|
+
/** Reports whether any answer differs from its acknowledged baseline. */
|
|
1352
1361
|
get dirty() {
|
|
1353
1362
|
return !matchesValues(this.values, this.#baseline);
|
|
1354
1363
|
}
|
|
@@ -1393,6 +1402,7 @@ var Form = class {
|
|
|
1393
1402
|
this.#batch(() => {
|
|
1394
1403
|
const moved = [];
|
|
1395
1404
|
for (const [name, answer] of entries) {
|
|
1405
|
+
this.#revisions.set(name, (this.#revisions.get(name) ?? 0) + 1);
|
|
1396
1406
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1397
1407
|
if (answer === void 0) delete this.#values[name];
|
|
1398
1408
|
else defineEntry(this.#values, name, cloneValue(answer));
|
|
@@ -1415,26 +1425,34 @@ var Form = class {
|
|
|
1415
1425
|
touch(name) {
|
|
1416
1426
|
this.#gate();
|
|
1417
1427
|
this.#requireField(name);
|
|
1418
|
-
this.#
|
|
1428
|
+
this.#batch(() => {
|
|
1429
|
+
if (this.#touched.has(name)) return;
|
|
1430
|
+
this.#touched.add(name);
|
|
1431
|
+
this.#emitter.emit("touch", name);
|
|
1432
|
+
});
|
|
1419
1433
|
}
|
|
1420
1434
|
/**
|
|
1421
1435
|
* Fails a field from outside, for what the rules cannot see.
|
|
1422
1436
|
*
|
|
1423
1437
|
* @param name - The field's name.
|
|
1424
1438
|
* @param message - What to tell the person.
|
|
1439
|
+
* @param submission - The active receipt to check against the field's captured revision.
|
|
1440
|
+
* @returns True if the failure was applied; false if the receipt or field revision is stale.
|
|
1425
1441
|
* @remarks
|
|
1426
1442
|
* One field holds one external failure: a second call replaces the first. The failure lasts
|
|
1427
1443
|
* until that field is filled again or the form is cleared.
|
|
1428
1444
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
1429
1445
|
* `FIELD` when the schema declares no such name.
|
|
1430
1446
|
*/
|
|
1431
|
-
invalidate(name, message) {
|
|
1432
|
-
this.#gate();
|
|
1447
|
+
invalidate(name, message, submission) {
|
|
1448
|
+
if (submission === void 0) this.#gate();
|
|
1433
1449
|
this.#requireField(name);
|
|
1450
|
+
if (submission !== void 0 && (this.#pending || submission !== this.#submission || !this.#captured.has(name) || this.#captured.get(name) !== (this.#revisions.get(name) ?? 0))) return false;
|
|
1434
1451
|
this.#batch(() => {
|
|
1435
1452
|
this.#invalidations.set(name, message);
|
|
1436
1453
|
if (this.#evaluate()) this.#emitter.emit("validate", this.#errors);
|
|
1437
1454
|
});
|
|
1455
|
+
return true;
|
|
1438
1456
|
}
|
|
1439
1457
|
/**
|
|
1440
1458
|
* Takes one or more fields out of the form.
|
|
@@ -1478,6 +1496,7 @@ var Form = class {
|
|
|
1478
1496
|
*/
|
|
1479
1497
|
submit() {
|
|
1480
1498
|
this.#gate();
|
|
1499
|
+
this.#gatePreparation();
|
|
1481
1500
|
return this.#batch(() => {
|
|
1482
1501
|
const changed = this.#evaluate();
|
|
1483
1502
|
const checked = this.#errors;
|
|
@@ -1497,12 +1516,16 @@ var Form = class {
|
|
|
1497
1516
|
const stopped = errors.length > 0;
|
|
1498
1517
|
if (stopped) {
|
|
1499
1518
|
const disabled = this.disabled;
|
|
1500
|
-
for (const field of this.#schema.fields) if (!disabled.has(field.name)
|
|
1519
|
+
for (const field of this.#schema.fields) if (!disabled.has(field.name) && !this.#touched.has(field.name)) {
|
|
1520
|
+
this.#touched.add(field.name);
|
|
1521
|
+
this.#emitter.emit("touch", field.name);
|
|
1522
|
+
}
|
|
1501
1523
|
}
|
|
1502
1524
|
if (stopped) return {
|
|
1503
1525
|
success: false,
|
|
1504
1526
|
error: errors
|
|
1505
1527
|
};
|
|
1528
|
+
this.#gatePreparation();
|
|
1506
1529
|
const answers = this.#snapshot();
|
|
1507
1530
|
this.#status = "settled";
|
|
1508
1531
|
this.#resolvers.resolve(answers);
|
|
@@ -1514,20 +1537,143 @@ var Form = class {
|
|
|
1514
1537
|
});
|
|
1515
1538
|
}
|
|
1516
1539
|
/**
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1540
|
+
* Prepares an owned enabled snapshot for acknowledgement while leaving the form editable.
|
|
1541
|
+
*
|
|
1542
|
+
* @returns The submission on success, or the errors that prevented preparation.
|
|
1543
|
+
* @remarks
|
|
1544
|
+
* A failed check refuses its checked errors even if an observer repairs them. A passing check
|
|
1545
|
+
* drains observer mutations through one further check. The receipt always carries that check's
|
|
1546
|
+
* snapshot, including when its notifications produce later edits. A throwing validator creates
|
|
1547
|
+
* no receipt. The pending identity is installed before `prepare` listeners run.
|
|
1548
|
+
* @throws A {@link FormError} coded `PENDING` while another preparation or receipt is pending,
|
|
1549
|
+
* or when clear interrupts preparation; terminal forms retain their terminal refusals.
|
|
1550
|
+
*/
|
|
1551
|
+
prepare() {
|
|
1552
|
+
this.#gate();
|
|
1553
|
+
this.#gatePreparation();
|
|
1554
|
+
const controller = new AbortController();
|
|
1555
|
+
this.#preparing = controller;
|
|
1556
|
+
return this.#batch(() => {
|
|
1557
|
+
try {
|
|
1558
|
+
let drained = false;
|
|
1559
|
+
while (true) {
|
|
1560
|
+
const values = this.values;
|
|
1561
|
+
const disabled = this.disabled;
|
|
1562
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1563
|
+
const touched = [];
|
|
1564
|
+
for (const field of this.#schema.fields) {
|
|
1565
|
+
if (disabled.has(field.name)) continue;
|
|
1566
|
+
captured.set(field.name, this.#revisions.get(field.name) ?? 0);
|
|
1567
|
+
if (!this.#touched.has(field.name)) touched.push(field.name);
|
|
1568
|
+
this.#touched.add(field.name);
|
|
1569
|
+
}
|
|
1570
|
+
const before = this.#evaluation;
|
|
1571
|
+
for (const name of touched) {
|
|
1572
|
+
if (controller.signal.aborted || this.#pending) break;
|
|
1573
|
+
this.#emitter.emit("touch", name);
|
|
1574
|
+
}
|
|
1575
|
+
this.#requirePreparation(controller);
|
|
1576
|
+
const errors = this.#check(values, disabled);
|
|
1577
|
+
const mutated = this.#evaluation !== before;
|
|
1578
|
+
const changed = !mutated && this.#record(errors);
|
|
1579
|
+
const evaluation = this.#evaluation;
|
|
1580
|
+
if (changed) this.#emitter.emit("validate", this.#errors);
|
|
1581
|
+
this.#requirePreparation(controller);
|
|
1582
|
+
if (errors.length > 0) return {
|
|
1583
|
+
success: false,
|
|
1584
|
+
error: errors
|
|
1585
|
+
};
|
|
1586
|
+
if (!drained && (mutated || this.#evaluation !== evaluation)) {
|
|
1587
|
+
drained = true;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
const submission = Object.freeze({
|
|
1591
|
+
values: this.#snapshot(values, disabled),
|
|
1592
|
+
signal: controller.signal
|
|
1593
|
+
});
|
|
1594
|
+
this.#captured = captured;
|
|
1595
|
+
this.#controller = controller;
|
|
1596
|
+
this.#submission = submission;
|
|
1597
|
+
this.#preparing = void 0;
|
|
1598
|
+
this.#emitter.emit("prepare", submission);
|
|
1599
|
+
return {
|
|
1600
|
+
success: true,
|
|
1601
|
+
value: submission
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
} finally {
|
|
1605
|
+
if (this.#preparing === controller) this.#preparing = void 0;
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Adopts the active submission's captured answers into the baseline without changing live state.
|
|
1611
|
+
*
|
|
1612
|
+
* @param submission - The receipt returned by preparation.
|
|
1613
|
+
* @returns True if this active receipt was accepted; false otherwise, including after teardown.
|
|
1614
|
+
*/
|
|
1615
|
+
accept(submission) {
|
|
1616
|
+
if (this.#pending || submission !== this.#submission) return false;
|
|
1617
|
+
return this.#batch(() => {
|
|
1618
|
+
const baseline = { ...this.#baseline };
|
|
1619
|
+
for (const name of this.#captured.keys()) {
|
|
1620
|
+
const value = Object.hasOwn(submission.values, name) ? submission.values[name] : void 0;
|
|
1621
|
+
if (value === void 0) delete baseline[name];
|
|
1622
|
+
else defineEntry(baseline, name, value);
|
|
1623
|
+
}
|
|
1624
|
+
this.#baseline = Object.freeze(baseline);
|
|
1625
|
+
this.#submission = void 0;
|
|
1626
|
+
this.#controller = void 0;
|
|
1627
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1628
|
+
this.#emitter.emit("accept", submission);
|
|
1629
|
+
return true;
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Consumes the active submission and aborts its signal without changing answers or baseline.
|
|
1634
|
+
*
|
|
1635
|
+
* @param submission - The receipt returned by preparation.
|
|
1636
|
+
* @returns True if this active receipt was aborted; false otherwise, including after teardown.
|
|
1637
|
+
*/
|
|
1638
|
+
abort(submission) {
|
|
1639
|
+
if (this.#pending || submission !== this.#submission) return false;
|
|
1640
|
+
this.#batch(() => {
|
|
1641
|
+
const controller = this.#controller;
|
|
1642
|
+
this.#submission = void 0;
|
|
1643
|
+
this.#controller = void 0;
|
|
1644
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1645
|
+
controller?.abort();
|
|
1646
|
+
this.#emitter.emit("abort", submission);
|
|
1647
|
+
});
|
|
1648
|
+
return true;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Returns every answer to its acknowledged baseline and aborts pending acknowledgement.
|
|
1519
1652
|
*
|
|
1520
1653
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
1521
1654
|
*/
|
|
1522
1655
|
clear() {
|
|
1523
1656
|
this.#gate();
|
|
1524
1657
|
this.#batch(() => {
|
|
1658
|
+
const submission = this.#submission;
|
|
1659
|
+
const controller = this.#controller;
|
|
1660
|
+
this.#submission = void 0;
|
|
1661
|
+
this.#controller = void 0;
|
|
1662
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1663
|
+
this.#preparing?.abort();
|
|
1525
1664
|
for (const name of Object.keys(this.#values)) delete this.#values[name];
|
|
1526
1665
|
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1527
1666
|
this.#touched.clear();
|
|
1528
1667
|
this.#disabled.clear();
|
|
1529
1668
|
this.#invalidations.clear();
|
|
1530
|
-
|
|
1669
|
+
this.#revisions.clear();
|
|
1670
|
+
let changed = false;
|
|
1671
|
+
try {
|
|
1672
|
+
changed = this.#evaluate();
|
|
1673
|
+
} finally {
|
|
1674
|
+
controller?.abort();
|
|
1675
|
+
if (submission !== void 0) this.#emitter.emit("abort", submission);
|
|
1676
|
+
}
|
|
1531
1677
|
this.#emitter.emit("clear");
|
|
1532
1678
|
if (changed) this.#emitter.emit("validate", this.#errors);
|
|
1533
1679
|
});
|
|
@@ -1550,6 +1696,13 @@ var Form = class {
|
|
|
1550
1696
|
if (this.#status === "settled") throw new FormError("SETTLED", "The form has settled and cannot change");
|
|
1551
1697
|
if (this.#status === "abandoned" || this.#pending) throw new FormError("ABANDONED", "The form was abandoned and cannot change");
|
|
1552
1698
|
}
|
|
1699
|
+
#gatePreparation() {
|
|
1700
|
+
if (this.#preparing !== void 0 || this.#submission !== void 0) throw new FormError("PENDING", "The form has a pending preparation or submission");
|
|
1701
|
+
}
|
|
1702
|
+
#requirePreparation(controller) {
|
|
1703
|
+
this.#gate();
|
|
1704
|
+
if (controller.signal.aborted) throw new FormError("PENDING", "The preparation was interrupted by clear");
|
|
1705
|
+
}
|
|
1553
1706
|
#requireField(name) {
|
|
1554
1707
|
const field = this.field(name);
|
|
1555
1708
|
if (field === void 0) throw new FormError("FIELD", `The schema declares no field named "${name}"`, { field: name });
|
|
@@ -1574,17 +1727,22 @@ var Form = class {
|
|
|
1574
1727
|
}
|
|
1575
1728
|
#evaluate() {
|
|
1576
1729
|
const disabled = this.disabled;
|
|
1730
|
+
return this.#record(this.#check(this.values, disabled));
|
|
1731
|
+
}
|
|
1732
|
+
#check(values, disabled) {
|
|
1577
1733
|
const options = this.#messages === void 0 ? { disabled } : {
|
|
1578
1734
|
messages: this.#messages,
|
|
1579
1735
|
disabled
|
|
1580
1736
|
};
|
|
1581
|
-
const errors = [...evaluateForm(this.#schema,
|
|
1737
|
+
const errors = [...evaluateForm(this.#schema, values, options)];
|
|
1582
1738
|
for (const [field, message] of this.#invalidations) if (!disabled.has(field)) errors.push(Object.freeze({
|
|
1583
1739
|
field,
|
|
1584
1740
|
message
|
|
1585
1741
|
}));
|
|
1742
|
+
return Object.freeze(errors);
|
|
1743
|
+
}
|
|
1744
|
+
#record(next) {
|
|
1586
1745
|
const previous = this.#errors;
|
|
1587
|
-
const next = Object.freeze(errors);
|
|
1588
1746
|
this.#errors = next;
|
|
1589
1747
|
const changed = next.length !== previous.length || next.some((error, index) => {
|
|
1590
1748
|
const before = previous[index];
|
|
@@ -1606,12 +1764,11 @@ var Form = class {
|
|
|
1606
1764
|
};
|
|
1607
1765
|
if (this.#status === "abandoned") this.#gate();
|
|
1608
1766
|
}
|
|
1609
|
-
#snapshot() {
|
|
1767
|
+
#snapshot(values = this.#values, disabled = this.disabled) {
|
|
1610
1768
|
const answers = {};
|
|
1611
|
-
const disabled = this.disabled;
|
|
1612
1769
|
for (const field of this.#schema.fields) {
|
|
1613
|
-
const value = Object.hasOwn(
|
|
1614
|
-
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1770
|
+
const value = Object.hasOwn(values, field.name) ? values[field.name] : void 0;
|
|
1771
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, cloneValue(value));
|
|
1615
1772
|
}
|
|
1616
1773
|
return Object.freeze(answers);
|
|
1617
1774
|
}
|
|
@@ -1625,11 +1782,20 @@ var Form = class {
|
|
|
1625
1782
|
}
|
|
1626
1783
|
}
|
|
1627
1784
|
#teardown() {
|
|
1628
|
-
|
|
1785
|
+
const submission = this.#submission;
|
|
1786
|
+
const controller = this.#controller;
|
|
1787
|
+
this.#submission = void 0;
|
|
1788
|
+
this.#controller = void 0;
|
|
1789
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1790
|
+
this.#preparing?.abort();
|
|
1791
|
+
const abandoned = this.#status === "editing";
|
|
1792
|
+
if (abandoned) {
|
|
1629
1793
|
this.#status = "abandoned";
|
|
1630
1794
|
this.#resolvers.reject(new FormError("ABANDONED", "The form was destroyed before it settled"));
|
|
1631
|
-
this.#emitter.emit("abandon");
|
|
1632
1795
|
}
|
|
1796
|
+
controller?.abort();
|
|
1797
|
+
if (submission !== void 0) this.#emitter.emit("abort", submission);
|
|
1798
|
+
if (abandoned) this.#emitter.emit("abandon");
|
|
1633
1799
|
this.#emitter.destroy();
|
|
1634
1800
|
}
|
|
1635
1801
|
};
|