aberdeen 1.5.0 → 1.7.0
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 +5 -11
- package/dist/aberdeen.d.ts +234 -128
- package/dist/aberdeen.js +212 -103
- package/dist/aberdeen.js.map +3 -3
- package/dist/dispatcher.d.ts +10 -7
- package/dist/dispatcher.js +11 -10
- package/dist/dispatcher.js.map +3 -3
- package/dist/route.d.ts +17 -0
- package/dist/route.js +65 -23
- package/dist/route.js.map +3 -3
- package/dist-min/aberdeen.js +9 -7
- package/dist-min/aberdeen.js.map +3 -3
- package/dist-min/dispatcher.js +2 -2
- package/dist-min/dispatcher.js.map +3 -3
- package/dist-min/route.js +2 -2
- package/dist-min/route.js.map +3 -3
- package/html-to-aberdeen +3 -6
- package/package.json +5 -2
- package/skill/SKILL.md +791 -206
- package/skill/aberdeen.md +2338 -0
- package/skill/dispatcher.md +129 -0
- package/skill/prediction.md +73 -0
- package/skill/route.md +277 -0
- package/skill/transitions.md +59 -0
- package/src/aberdeen.ts +490 -244
- package/src/dispatcher.ts +16 -13
- package/src/route.ts +93 -22
- package/skill/references/prediction.md +0 -45
- package/skill/references/routing.md +0 -81
- package/skill/references/transitions.md +0 -52
package/src/aberdeen.ts
CHANGED
|
@@ -53,7 +53,7 @@ function queue(runner: QueueRunner) {
|
|
|
53
53
|
* ```typescript
|
|
54
54
|
* const data = proxy("before");
|
|
55
55
|
*
|
|
56
|
-
* $('#'
|
|
56
|
+
* $('#', data);
|
|
57
57
|
* console.log(1, document.body.innerHTML); // before
|
|
58
58
|
*
|
|
59
59
|
* // Make an update that should cause the DOM to change.
|
|
@@ -166,9 +166,7 @@ abstract class Scope implements QueueRunner {
|
|
|
166
166
|
|
|
167
167
|
[ptr: ReverseSortedSetPointer]: this;
|
|
168
168
|
|
|
169
|
-
onChange(index: any): void
|
|
170
|
-
queue(this);
|
|
171
|
-
}
|
|
169
|
+
abstract onChange(target: TargetType, index: any, newData: any, oldData: any): void;
|
|
172
170
|
abstract queueRun(): void;
|
|
173
171
|
|
|
174
172
|
abstract getLastNode(): Node | undefined;
|
|
@@ -213,6 +211,7 @@ abstract class ContentScope extends Scope {
|
|
|
213
211
|
|
|
214
212
|
abstract svg: boolean;
|
|
215
213
|
abstract el: Element;
|
|
214
|
+
private changes: undefined | Map<TargetType, Map<any, any>>; // target => (index => oldData)
|
|
216
215
|
|
|
217
216
|
constructor(
|
|
218
217
|
cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)> = [],
|
|
@@ -246,7 +245,42 @@ abstract class ContentScope extends Scope {
|
|
|
246
245
|
this.lastChild = undefined;
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
onChange(target: TargetType, index: any, newData: any, oldData: any): void {
|
|
249
|
+
if (!this.changes) {
|
|
250
|
+
this.changes = new Map();
|
|
251
|
+
queue(this);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let targetDelta = this.changes.get(target);
|
|
255
|
+
if (!targetDelta) {
|
|
256
|
+
targetDelta = new Map();
|
|
257
|
+
this.changes.set(target, targetDelta);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (targetDelta.has(index)) {
|
|
261
|
+
// Already changed before, keep original oldData
|
|
262
|
+
// Unless it changed back to original value
|
|
263
|
+
if (targetDelta.get(index) === newData) targetDelta.delete(index);
|
|
264
|
+
} else {
|
|
265
|
+
targetDelta.set(index, oldData);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fetchHasChanges(): boolean {
|
|
270
|
+
if (!this.changes) return false;
|
|
271
|
+
for(const targetDelta of this.changes.values()) {
|
|
272
|
+
if (targetDelta.size > 0) {
|
|
273
|
+
delete this.changes;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
delete this.changes;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
249
282
|
queueRun() {
|
|
283
|
+
if (!this.fetchHasChanges()) return;
|
|
250
284
|
this.remove();
|
|
251
285
|
|
|
252
286
|
topRedrawScope = this;
|
|
@@ -258,10 +292,6 @@ abstract class ContentScope extends Scope {
|
|
|
258
292
|
return this.getLastNode() || this.getPrecedingNode();
|
|
259
293
|
}
|
|
260
294
|
|
|
261
|
-
onChange() {
|
|
262
|
-
queue(this);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
295
|
getChildPrevSibling() {
|
|
266
296
|
return this.lastChild;
|
|
267
297
|
}
|
|
@@ -479,11 +509,10 @@ class OnEachScope extends Scope {
|
|
|
479
509
|
byIndex: Map<any, OnEachItemScope> = new Map();
|
|
480
510
|
|
|
481
511
|
/** The reverse-ordered list of item scopes, not including those for which makeSortKey returned undefined. */
|
|
482
|
-
sortedSet: ReverseSortedSet<OnEachItemScope, "sortKey"> =
|
|
483
|
-
new ReverseSortedSet("sortKey");
|
|
512
|
+
sortedSet: ReverseSortedSet<OnEachItemScope, "sortKey"> = new ReverseSortedSet("sortKey");
|
|
484
513
|
|
|
485
514
|
/** Indexes that have been created/removed and need to be handled in the next `queueRun`. */
|
|
486
|
-
changedIndexes:
|
|
515
|
+
changedIndexes: Map<any, any> = new Map(); // index => old value
|
|
487
516
|
|
|
488
517
|
constructor(
|
|
489
518
|
proxy: TargetType,
|
|
@@ -493,8 +522,7 @@ class OnEachScope extends Scope {
|
|
|
493
522
|
public makeSortKey?: (value: any, key: any) => SortKeyType,
|
|
494
523
|
) {
|
|
495
524
|
super();
|
|
496
|
-
const target: TargetType = (this.target =
|
|
497
|
-
(proxy as any)[TARGET_SYMBOL] || proxy);
|
|
525
|
+
const target: TargetType = (this.target = (proxy as any)[TARGET_SYMBOL] || proxy);
|
|
498
526
|
|
|
499
527
|
subscribe(target, ANY_SYMBOL, this);
|
|
500
528
|
this.prevSibling = currentScope.getChildPrevSibling();
|
|
@@ -518,16 +546,27 @@ class OnEachScope extends Scope {
|
|
|
518
546
|
return findLastNodeInPrevSiblings(this.prevSibling);
|
|
519
547
|
}
|
|
520
548
|
|
|
521
|
-
onChange(index: any) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
549
|
+
onChange(target: TargetType, index: any, newData: any, oldData: any) {
|
|
550
|
+
// target === this.target
|
|
551
|
+
if (!(target instanceof Array) || typeof index === "number") {
|
|
552
|
+
if (this.changedIndexes.has(index)) {
|
|
553
|
+
if (this.changedIndexes.get(index) === newData) {
|
|
554
|
+
// Data changed back to original value, so ignore it
|
|
555
|
+
this.changedIndexes.delete(index);
|
|
556
|
+
}
|
|
557
|
+
// Else, data changed a second time
|
|
558
|
+
} else {
|
|
559
|
+
// Initial data change
|
|
560
|
+
this.changedIndexes.set(index, oldData);
|
|
561
|
+
queue(this);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
525
564
|
}
|
|
526
565
|
|
|
527
566
|
queueRun() {
|
|
528
567
|
const indexes = this.changedIndexes;
|
|
529
|
-
this.changedIndexes = new
|
|
530
|
-
for (const index of indexes) {
|
|
568
|
+
this.changedIndexes = new Map();
|
|
569
|
+
for (const index of indexes.keys()) {
|
|
531
570
|
const oldScope = this.byIndex.get(index);
|
|
532
571
|
if (oldScope) oldScope.remove();
|
|
533
572
|
|
|
@@ -633,6 +672,8 @@ class OnEachItemScope extends ContentScope {
|
|
|
633
672
|
/* c8 ignore next */
|
|
634
673
|
if (currentScope !== ROOT_SCOPE) internalError(4);
|
|
635
674
|
|
|
675
|
+
if (!this.fetchHasChanges()) return;
|
|
676
|
+
|
|
636
677
|
// We're not calling `remove` here, as we don't want to remove ourselves from
|
|
637
678
|
// the sorted set. `redraw` will take care of that, if needed.
|
|
638
679
|
// Also, we can't use `getLastNode` here, as we've hacked it to return the
|
|
@@ -959,23 +1000,27 @@ export function isEmpty(proxied: TargetType): boolean {
|
|
|
959
1000
|
|
|
960
1001
|
if (target instanceof Array) {
|
|
961
1002
|
subscribe(target, "length", (index: any, newData: any, oldData: any) => {
|
|
962
|
-
if (!newData !== !oldData)
|
|
1003
|
+
if (!newData !== !oldData) scope.onChange(target, EMPTY, !newData, !oldData);
|
|
963
1004
|
});
|
|
964
1005
|
return !target.length;
|
|
965
1006
|
}
|
|
966
1007
|
|
|
967
1008
|
if (target instanceof Map) {
|
|
968
1009
|
subscribe(target, MAP_SIZE_SYMBOL, (index: any, newData: any, oldData: any) => {
|
|
969
|
-
if (!newData !== !oldData)
|
|
1010
|
+
if (!newData !== !oldData) scope.onChange(target, EMPTY, !newData, !oldData);
|
|
970
1011
|
});
|
|
971
1012
|
return !target.size;
|
|
972
1013
|
}
|
|
973
1014
|
|
|
974
|
-
|
|
1015
|
+
let oldEmpty = isObjEmpty(target);
|
|
975
1016
|
subscribe(target, ANY_SYMBOL, (index: any, newData: any, oldData: any) => {
|
|
976
|
-
if (
|
|
1017
|
+
if ((newData === EMPTY) !== (oldData === EMPTY)) {
|
|
1018
|
+
const newEmpty = isObjEmpty(target);
|
|
1019
|
+
scope.onChange(target, EMPTY, newEmpty, oldEmpty);
|
|
1020
|
+
oldEmpty = newEmpty;
|
|
1021
|
+
}
|
|
977
1022
|
});
|
|
978
|
-
return
|
|
1023
|
+
return oldEmpty;
|
|
979
1024
|
}
|
|
980
1025
|
|
|
981
1026
|
/** @private */
|
|
@@ -1050,7 +1095,7 @@ export function defaultEmitHandler(
|
|
|
1050
1095
|
if (byIndex) {
|
|
1051
1096
|
for (const observer of byIndex) {
|
|
1052
1097
|
if (typeof observer === "function") observer(index, newData, oldData);
|
|
1053
|
-
else observer.onChange(index);
|
|
1098
|
+
else observer.onChange(target, index, newData, oldData);
|
|
1054
1099
|
}
|
|
1055
1100
|
}
|
|
1056
1101
|
}
|
|
@@ -1516,8 +1561,6 @@ function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
|
|
|
1516
1561
|
* Like {@link copy}, but uses merge semantics. Properties in `dst` not present in `src` are kept.
|
|
1517
1562
|
* `null`/`undefined` in `src` delete properties in `dst`.
|
|
1518
1563
|
*
|
|
1519
|
-
* When the destination is an object and the source is an array, its keys are used as (sparse) array indices.
|
|
1520
|
-
*
|
|
1521
1564
|
* @example Basic merge
|
|
1522
1565
|
* ```typescript
|
|
1523
1566
|
* const source = { b: { c: 99 }, d: undefined }; // d: undefined will delete
|
|
@@ -1528,14 +1571,6 @@ function copySet(dst: any, dstKey: any, src: any, flags: number): boolean {
|
|
|
1528
1571
|
* console.log(dest); // proxy({ a: 1, b: { c: 99, x: 5, y: 6 }, c: { z: 7 } })
|
|
1529
1572
|
* ```
|
|
1530
1573
|
*
|
|
1531
|
-
* @example Partial Array Merge
|
|
1532
|
-
* ```typescript
|
|
1533
|
-
* const messages = proxy(['msg1', 'msg2', 'msg3']);
|
|
1534
|
-
* const update = { 1: 'updated msg2' }; // Update using object key as index
|
|
1535
|
-
* merge(messages, update);
|
|
1536
|
-
* console.log(messages); // proxy(['msg1', 'updated msg2', 'msg3'])
|
|
1537
|
-
* ```
|
|
1538
|
-
*
|
|
1539
1574
|
*/
|
|
1540
1575
|
export function merge<T extends object>(dst: T, value: Partial<T>): boolean;
|
|
1541
1576
|
export function merge<T extends object>(dst: T, dstKey: keyof T, value: Partial<T[typeof dstKey]>): boolean;
|
|
@@ -1644,7 +1679,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
|
|
|
1644
1679
|
const old = dst.get(k);
|
|
1645
1680
|
dst.delete(k);
|
|
1646
1681
|
if (flags & COPY_EMIT) {
|
|
1647
|
-
emit(dst, k,
|
|
1682
|
+
emit(dst, k, EMPTY, old);
|
|
1648
1683
|
}
|
|
1649
1684
|
changed = true;
|
|
1650
1685
|
}
|
|
@@ -1677,7 +1712,7 @@ function copyRecursive<T extends object>(dst: T, src: T, flags: number): boolean
|
|
|
1677
1712
|
const old = dst[k];
|
|
1678
1713
|
delete dst[k];
|
|
1679
1714
|
if (flags & COPY_EMIT && old !== undefined) {
|
|
1680
|
-
emit(dst, k,
|
|
1715
|
+
emit(dst, k, EMPTY, old);
|
|
1681
1716
|
}
|
|
1682
1717
|
changed = true;
|
|
1683
1718
|
}
|
|
@@ -1704,33 +1739,148 @@ export const NO_COPY = Symbol("NO_COPY");
|
|
|
1704
1739
|
(Promise.prototype as any)[NO_COPY] = true;
|
|
1705
1740
|
|
|
1706
1741
|
/**
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1742
|
+
* A reactive object containing CSS variable definitions.
|
|
1743
|
+
*
|
|
1744
|
+
* Any property you assign to `cssVars` becomes available as a CSS custom property throughout your application.
|
|
1745
|
+
*
|
|
1746
|
+
* Use {@link setSpacingCssVars} to optionally initialize `cssVars[1]` through `cssVars[12]` with an exponential spacing scale.
|
|
1747
|
+
*
|
|
1748
|
+
* When you reference a CSS variable in Aberdeen using the `$` prefix (e.g., `$primary`), it automatically resolves to `var(--primary)`.
|
|
1749
|
+
* For numeric keys (which can't be used directly as CSS custom property names), Aberdeen prefixes them with `m` (e.g., `$3` becomes `var(--m3)`).
|
|
1750
|
+
*
|
|
1751
|
+
* When you add the first property to cssVars, Aberdeen automatically creates a reactive `<style>` tag in `<head>`
|
|
1752
|
+
* containing the `:root` CSS custom property declarations. The style tag is automatically removed if cssVars becomes empty.
|
|
1753
|
+
*
|
|
1715
1754
|
* @example
|
|
1716
|
-
* ```
|
|
1755
|
+
* ```javascript
|
|
1756
|
+
* import { cssVars, setSpacingCssVars, $ } from 'aberdeen';
|
|
1757
|
+
*
|
|
1758
|
+
* // Optionally initialize spacing scale
|
|
1759
|
+
* setSpacingCssVars(); // Uses defaults: base=1, unit='rem'
|
|
1760
|
+
*
|
|
1761
|
+
* // Define custom colors - style tag is automatically created
|
|
1717
1762
|
* cssVars.primary = '#3b82f6';
|
|
1718
|
-
* cssVars
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1763
|
+
* cssVars.danger = '#ef4444';
|
|
1764
|
+
*
|
|
1765
|
+
* // Use in elements with the $ prefix
|
|
1766
|
+
* $('button bg:$primary fg:white');
|
|
1767
|
+
*
|
|
1768
|
+
* // Use spacing (if setSpacingCssVars() was called)
|
|
1769
|
+
* $('div mt:$3'); // Sets margin-top to var(--m3)
|
|
1721
1770
|
* ```
|
|
1722
1771
|
*/
|
|
1723
1772
|
export const cssVars: Record<string, string> = optProxy({});
|
|
1724
1773
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1774
|
+
/**
|
|
1775
|
+
* Initializes `cssVars[0]` through `cssVars[12]` with an exponential spacing scale.
|
|
1776
|
+
*
|
|
1777
|
+
* The scale is calculated as `2^(n-3) * base`, providing values from `0.25 * base` to `512 * base`.
|
|
1778
|
+
*
|
|
1779
|
+
* @param base - The base size for the spacing scale that will apply to `cssVars[3]`. Every step up the scale will double this, while every step down will halve it. Defaults to 1.
|
|
1780
|
+
* @param unit - The CSS unit to use, like 'rem', 'em', or 'px'. Defaults to 'rem'.
|
|
1781
|
+
*
|
|
1782
|
+
* @example
|
|
1783
|
+
* ```javascript
|
|
1784
|
+
* import { setSpacingCssVars, cssVars, onEach, $} from 'aberdeen';
|
|
1785
|
+
* // Use default scale (0.25rem to 512rem)
|
|
1786
|
+
* setSpacingCssVars();
|
|
1787
|
+
*
|
|
1788
|
+
* // Use custom base size
|
|
1789
|
+
* setSpacingCssVars(16, 'px'); // 4px to 8192px
|
|
1790
|
+
*
|
|
1791
|
+
* // Use em units
|
|
1792
|
+
* setSpacingCssVars(1, 'em'); // 0.25em to 512em
|
|
1793
|
+
*
|
|
1794
|
+
* // Show the last generated spacing values
|
|
1795
|
+
* onEach(cssVars, (value, key) => {
|
|
1796
|
+
* $(`div #${key} → ${value}`)
|
|
1797
|
+
* }, (value, key) => parseInt(key)); // Numeric sort
|
|
1798
|
+
* ```
|
|
1799
|
+
*/
|
|
1800
|
+
export function setSpacingCssVars(base = 1, unit = 'rem'): void {
|
|
1801
|
+
for (let i = 0; i <= 12; i++) {
|
|
1802
|
+
cssVars[i] = 2 ** (i - 3) * base + unit;
|
|
1803
|
+
}
|
|
1727
1804
|
}
|
|
1728
1805
|
|
|
1806
|
+
// Matches: (1) parenthesized content, (2) quoted content, (3) $varName at start or after space
|
|
1807
|
+
const CSS_VAR_PATTERN = /(\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
|
|
1729
1808
|
const DIGIT_FIRST = /^\d/;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* Expands all `$varName` patterns in a CSS value to `var(--varName)`.
|
|
1812
|
+
* Only matches `$` at the start of the value or after a space.
|
|
1813
|
+
* Content inside parentheses or quotes is preserved as-is.
|
|
1814
|
+
* Numeric names get an 'm' prefix (e.g., `$3` → `var(--m3)`).
|
|
1815
|
+
*/
|
|
1816
|
+
function cssVarRef(value: string): string {
|
|
1817
|
+
if (value.indexOf('$') < 0) return value;
|
|
1818
|
+
return value.replace(CSS_VAR_PATTERN, (match, parens, quoted, prefix, name) => {
|
|
1819
|
+
if (parens || quoted) return match;
|
|
1820
|
+
const varName = DIGIT_FIRST.test(name) ? `m${name}` : name;
|
|
1821
|
+
return `${prefix}var(--${varName})`;
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Automatically mount cssVars style tag to document.head when cssVars is not empty
|
|
1826
|
+
if (typeof document !== "undefined") {
|
|
1827
|
+
leakScope(() => {
|
|
1828
|
+
$(() => {
|
|
1829
|
+
if (!isEmpty(cssVars)) {
|
|
1830
|
+
mount(document.head, () => {
|
|
1831
|
+
$('style', () => {
|
|
1832
|
+
let css = ":root {\n";
|
|
1833
|
+
for(const [key, value] of Object.entries(cssVars)) {
|
|
1834
|
+
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
|
|
1835
|
+
css += ` --${varName}: ${value};\n`;
|
|
1836
|
+
}
|
|
1837
|
+
css += "}";
|
|
1838
|
+
$(`#${css}`);
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
|
|
1847
|
+
let darkModeState: {value: boolean} | undefined;
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Returns whether the user's browser prefers a dark color scheme.
|
|
1851
|
+
*
|
|
1852
|
+
* This function is reactive - scopes that call it will re-execute when the
|
|
1853
|
+
* browser's color scheme preference changes (via the `prefers-color-scheme` media query).
|
|
1854
|
+
*
|
|
1855
|
+
* Use this in combination with {@link $} and {@link cssVars} to implement theme switching:
|
|
1856
|
+
*
|
|
1857
|
+
* @returns `true` if the browser prefers dark mode, `false` if it prefers light mode.
|
|
1858
|
+
*
|
|
1859
|
+
* @example
|
|
1860
|
+
* ```javascript
|
|
1861
|
+
* import { darkMode, cssVars, $, mount } from 'aberdeen';
|
|
1862
|
+
*
|
|
1863
|
+
* // Reactively set colors based on browser preference
|
|
1864
|
+
* $(() => {
|
|
1865
|
+
* cssVars.bg = darkMode() ? '#1a1a1a' : '#ffffff';
|
|
1866
|
+
* cssVars.fg = darkMode() ? '#e5e5e5' : '#000000';
|
|
1867
|
+
* });
|
|
1868
|
+
*
|
|
1869
|
+
* $('div bg:$bg fg:$fg p:1rem #Colors change based on system dark mode preference');
|
|
1870
|
+
* ```
|
|
1871
|
+
*/
|
|
1872
|
+
export function darkMode(): boolean {
|
|
1873
|
+
if (!darkModeState) {
|
|
1874
|
+
// Initialize on first use
|
|
1875
|
+
|
|
1876
|
+
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
1877
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1878
|
+
|
|
1879
|
+
darkModeState = proxy({ value: mediaQuery.matches });
|
|
1880
|
+
mediaQuery.addEventListener('change', () => darkModeState!.value = mediaQuery.matches);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
return darkModeState.value;
|
|
1734
1884
|
}
|
|
1735
1885
|
|
|
1736
1886
|
/**
|
|
@@ -1833,12 +1983,9 @@ function applyBind(el: HTMLInputElement, target: any) {
|
|
|
1833
1983
|
};
|
|
1834
1984
|
} else {
|
|
1835
1985
|
onInputChange = () => {
|
|
1836
|
-
target.value =
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
? null
|
|
1840
|
-
: +el.value
|
|
1841
|
-
: el.value;
|
|
1986
|
+
target.value = type === "number" || type === "range"
|
|
1987
|
+
? el.value === "" ? null : +el.value
|
|
1988
|
+
: el.value;
|
|
1842
1989
|
};
|
|
1843
1990
|
if (value === undefined) onInputChange();
|
|
1844
1991
|
onProxyChange = () => {
|
|
@@ -1894,38 +2041,62 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
|
|
|
1894
2041
|
*
|
|
1895
2042
|
* @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
|
|
1896
2043
|
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
* -
|
|
1906
|
-
*
|
|
1907
|
-
* -
|
|
1908
|
-
*
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
2044
|
+
* ### String arguments
|
|
2045
|
+
* Strings can be used to create and insert new elements, set classnames for the *current* element, and add text to the current element.
|
|
2046
|
+
* The format of a string is: (**tag** | `.` **class** | **key**[=:]**val** | **key**[=:]"**val containing spaces**")* ('#' **text** | **key**[=:])?
|
|
2047
|
+
*
|
|
2048
|
+
* So a string may consist of any number of...
|
|
2049
|
+
* - **tag** elements, like `h1` or `div`. These elements are created, added to the *current* element, and become the new *current* element for the rest of this `$` function execution.
|
|
2050
|
+
* - CSS classes prefixed by `.` characters. These classes will be added to the *current* element. Optionally, CSS classes can be appended to a **tag** without a space. So both `div.myclass` and `div .myclass` are valid and do the same thing.
|
|
2051
|
+
* - Property key/value pairs, like `type=password`, `placeholder="Your name"` or `data-id=123`. When the value contains spaces, it needs to be quoted with either "double quotes", 'single quotes' or \`backticks\`. Quotes within quoted values cannot be escaped (see the next rule for a solution). Key/value pairs will be handled according to *Property rules* below, but with the caveat that values can only be strings.
|
|
2052
|
+
* - CSS key/value pairs using two syntaxes:
|
|
2053
|
+
* - **Short form** `key:value` (no space after colon): The value ends at the next whitespace. Example: `m:$3 bg:red r:8px`
|
|
2054
|
+
* - **Long form** `key: value;` (space after colon): The value continues until a semicolon. Example: `box-shadow: 2px 0 6px black; transition: all 0.3s ease;`
|
|
2055
|
+
*
|
|
2056
|
+
* Both forms support CSS shortcuts (see below). You can mix them: `m:$3 box-shadow: 0 2px 4px rgba(0,0,0,0.2); bg:$cardBg`
|
|
2057
|
+
* The elements must be separated by spaces, except before a `.cssClass` if it is preceded by either **tag** or another CSS class.
|
|
2058
|
+
*
|
|
2059
|
+
* And a string may end in...
|
|
2060
|
+
* - A '#' followed by text, which will be added as a `TextNode` to the *current* element. The text ranges til the end of the string, and may contain any characters, including spaces and quotes.
|
|
2061
|
+
* - A key followed by an '=' character, in which case the value is expected as a separate argument. The key/value pair is set according to the *Property rules* below. This is useful when the value is not a string or contains spaces or user data. Example: `$('button text="Click me" click=', () => alert('Clicked!'))` or `$('input.value=', someUserData, "placeholder=", "Type your stuff")`. In case the value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope.
|
|
2062
|
+
* - A key followed by a ':' character (with no value), in which case the value is expected as a separate argument. The value is treated as a CSS value to be set inline on the *current* element. Example: `$('div margin-top:', someValueInPx)`. In case the value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope.
|
|
2063
|
+
*
|
|
2064
|
+
* ### Function arguments
|
|
2065
|
+
* When a function (without arguments nor a return value) is passed in, it will be reactively executed in its own observer scope, preserving the *current* element. So any `$()` invocations within this function will add child elements to or set properties on that element. If the function reads observable data, and that data is changed later on, the function we re-execute (after side effects, such as DOM modifications through `$`, have been cleaned - see also {@link clean}).
|
|
2066
|
+
*
|
|
2067
|
+
* ### Object arguments
|
|
2068
|
+
* When an object is passed in, its key-value pairs are used to modify the *current* element according to the *Property rules* below, *unless* the key starts with a `$` character, in which case that character is stripped of and the key/value pair is treated as a CSS property, subject to the *CSS shortcuts* below. In case a value is a proxied object, its `.value` property will be applied reactively without needing to rerender the parent scope. In most cases, the string notation (`key=` and `key:`) is preferred over this object notation, for readability.
|
|
2069
|
+
*
|
|
2070
|
+
* ### DOM node arguments
|
|
2071
|
+
* When a DOM Node (Element or TextNode) is passed in, it is added as a child to the *current* element. If the Node is an Element, it becomes the new *current* element for the rest of this `$` function execution.
|
|
2072
|
+
*
|
|
2073
|
+
* ### Property rules
|
|
2074
|
+
* - **Attribute:** The common case is setting the value as an HTML attribute named key. For example `$('input placeholder=Name')` results in `<input placeholder="Name">`.
|
|
2075
|
+
* - **Event listener:** If the value is a `function` it is set as an event listener for the event with the name given by the key. For example: $('button text=Press! click=', () => alert('Clicked!'))`. The event listener will be removed when the current scope is destroyed.
|
|
2076
|
+
* - **DOM property:** When the value is a boolean, or the key is `"value"` or `"selectedIndex"`, it is set on the `current` element as a DOM property instead of an HTML attribute. For example `$('checked=', true)` would do `el.checked = true` for the *current* element.
|
|
2077
|
+
* - **Conditional CSS class:** If the key starts with a `.` character, its either added to or removed from the *current* element as a CSS class, based on the truthiness of the value. So `$('.hidden=', isHidden)` would toggle the `hidden` CSS class. This only works if the `=` is the last character of the string, and the next argument is the value. Its common for the value to be a proxied object, in which case its `.value` is reactively applied without needing to rerender the parent scope.
|
|
2078
|
+
* - **Create transition:** When the key is `"create"`, the value will be added as a CSS class to the *current* element immediately, and then removed right after the browser has finished doing a layout pass. This behavior only triggers when the scope setting the `create` is the top-level scope being (re-)run. This allows for creation transitions, without triggering the transitions for deeply nested elements being drawn as part of a larger component. The string may also contain multiple dot-separated CSS classes, such as `.fade.grow`. The initial dot is optional. Alternatively, to allow for more complex transitions, the value may be a function that receives the `HTMLElement` being created as its only argument. It is *only* called if this is the top-level element being created in this scope run. See `transitions.ts` in the Aberdeen source code for some examples.
|
|
2079
|
+
* - **Destroy transition:** When the key is `"destroy"` the value will be used to apply a CSS transition if the *current* element is later on removed from the DOM and is the top-level element to be removed. This happens as follows: actual removal from the DOM is delayed by 2 seconds, and in the mean-time the value string is added as a CSS class to the element, allowing for a deletion transition. The string may also contain multiple dot-separated CSS classes, such as `.fade.shrink`. The initial dot is optional. Alternatively, to allow for more complex transitions, the value may be a function that receives the `HTMLElement` to be removed from the DOM as its only argument. This function may perform any transitions and is then itself responsible for eventually removing the element from the DOM. See `transitions.ts` in the Aberdeen source code for some examples.
|
|
2080
|
+
* - **Two-way data binding:** When the key is `"bind"` a two-way binding between the `.value` property of the given proxied object, and the *current* input element (`<input>`, `<select>` or `<textarea>`) is created. This is often used together with {@link ref}, in order to use properties other than `.value`.
|
|
2081
|
+
* - **Text:**: If the key is `"text"`, the value will be appended as a `TextNode` to the *current* element. The same can also be done with the `#` syntax in string arguments, though `text=` allows additional properties to come after in the same string: `$('button text=Hello click=', alert)`.
|
|
2082
|
+
* - **Unsafe HTML:** When the key is `"html"`, the value will be added as HTML to the *current* element. This should only be used in exceptional situations. Beware of XSS! Never use this with untrusted user data.
|
|
2083
|
+
*
|
|
2084
|
+
* ### CSS shortcuts
|
|
2085
|
+
* For conciseness, Aberdeen supports some CSS shortcuts when setting CSS properties.
|
|
2086
|
+
* | Shortcut | Expands to |
|
|
2087
|
+
* |----------|------------|
|
|
2088
|
+
* | `m`, `mt`, `mb`, `ml`, `mr` | `margin`, `margin-top`, `margin-bottom`, `margin-left`, `margin-right` |
|
|
2089
|
+
* | `mv`, `mh` | Vertical (top+bottom) or horizontal (left+right) margins |
|
|
2090
|
+
* | `p`, `pt`, `pb`, `pl`, `pr` | `padding`, `padding-top`, `padding-bottom`, `padding-left`, `padding-right` |
|
|
2091
|
+
* | `pv`, `ph` | Vertical or horizontal padding |
|
|
2092
|
+
* | `w`, `h` | `width`, `height` |
|
|
2093
|
+
* | `bg` | `background` |
|
|
2094
|
+
* | `fg` | `color` |
|
|
2095
|
+
* | `r` | `border-radius` |
|
|
2096
|
+
*
|
|
2097
|
+
* Also, when the value is a string starting with `$`, it is treated as a reference to a CSS variable, expanding to `var(--variableName)`. For numeric variable names (which can't be used directly as CSS custom property names), Aberdeen prefixes them with `m`, so `$3` expands to `var(--m3)`. This is primarily intended for use with {@link setSpacingCssVars}, which initializes spacing variables named `0` through `12` with an exponential spacing scale.
|
|
2098
|
+
*
|
|
2099
|
+
* @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions), or the current element if no new element was created. You should normally not need to use the return value - use this function's DOM manipulation abilities instead. One valid use case is when integrating with non-Aberdeen code that requires a reference to a DOM element.
|
|
1929
2100
|
*
|
|
1930
2101
|
* @example Create Element
|
|
1931
2102
|
* ```typescript
|
|
@@ -1972,6 +2143,14 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
|
|
|
1972
2143
|
* }
|
|
1973
2144
|
* });
|
|
1974
2145
|
* ```
|
|
2146
|
+
*
|
|
2147
|
+
* @example Proxied objects as values
|
|
2148
|
+
* ```typescript
|
|
2149
|
+
* const myColor = proxy('red');
|
|
2150
|
+
* $('p text="The color is " text=', myColor, 'click=', () => myColor.value = 'yellow')
|
|
2151
|
+
* // Clicking the text will cause it to change color without recreating the <p> itself
|
|
2152
|
+
* ```
|
|
2153
|
+
* This is often used together with {@link ref}, in order to use properties other than `.value`.
|
|
1975
2154
|
*/
|
|
1976
2155
|
|
|
1977
2156
|
export function $(...args: any[]): undefined | Element {
|
|
@@ -1990,16 +2169,37 @@ export function $(...args: any[]): undefined | Element {
|
|
|
1990
2169
|
nextPos = findFirst(arg, " .=:#", pos);
|
|
1991
2170
|
const next = arg[nextPos];
|
|
1992
2171
|
|
|
1993
|
-
if (next === ":"
|
|
1994
|
-
|
|
1995
|
-
|
|
2172
|
+
if (next === ":") {
|
|
2173
|
+
// Style property: key:value or key: value;
|
|
2174
|
+
const key = '$' + arg.substring(pos, nextPos);
|
|
2175
|
+
if (nextPos + 1 >= argLen) {
|
|
2176
|
+
applyArg(el, key, args[++argIndex]);
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
if (arg[nextPos + 1] === ' ') {
|
|
2180
|
+
// Long form: "key: value;" - read until semicolon
|
|
2181
|
+
const endIndex = findFirst(arg, ";", nextPos + 2);
|
|
2182
|
+
const value = arg.substring(nextPos + 2, endIndex).trim();
|
|
2183
|
+
applyArg(el, key, value);
|
|
2184
|
+
nextPos = endIndex;
|
|
2185
|
+
} else {
|
|
2186
|
+
// Short form: "key:value" - read until whitespace
|
|
2187
|
+
const endIndex = findFirst(arg, " ", nextPos + 1);
|
|
2188
|
+
const value = arg.substring(nextPos + 1, endIndex);
|
|
2189
|
+
applyArg(el, key, value);
|
|
2190
|
+
nextPos = endIndex;
|
|
2191
|
+
}
|
|
2192
|
+
} else if (next === "=") {
|
|
2193
|
+
// Attribute: key=value or key="quoted value"
|
|
2194
|
+
const key = arg.substring(pos, nextPos);
|
|
1996
2195
|
if (nextPos + 1 >= argLen) {
|
|
1997
2196
|
applyArg(el, key, args[++argIndex]);
|
|
1998
2197
|
break;
|
|
1999
2198
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
const
|
|
2199
|
+
const afterEquals = arg[nextPos + 1];
|
|
2200
|
+
if (afterEquals === '"' || afterEquals === "'" || afterEquals === "`") {
|
|
2201
|
+
const endIndex = findFirst(arg, afterEquals, nextPos + 2);
|
|
2202
|
+
const value = arg.substring(nextPos + 2, endIndex);
|
|
2003
2203
|
applyArg(el, key, value);
|
|
2004
2204
|
nextPos = endIndex;
|
|
2005
2205
|
} else {
|
|
@@ -2031,6 +2231,7 @@ export function $(...args: any[]): undefined | Element {
|
|
|
2031
2231
|
applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
|
|
2032
2232
|
nextPos = classEnd;
|
|
2033
2233
|
} else {
|
|
2234
|
+
// An unconditional class name
|
|
2034
2235
|
let className: any = arg.substring(nextPos + 1, classEnd);
|
|
2035
2236
|
el.classList.add(className || args[++argIndex]);
|
|
2036
2237
|
nextPos = classEnd - 1;
|
|
@@ -2080,126 +2281,222 @@ let cssCount = 0;
|
|
|
2080
2281
|
/**
|
|
2081
2282
|
* Inserts CSS rules into the document, scoping them with a unique class name.
|
|
2082
2283
|
*
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2284
|
+
* The `style` parameter can be either:
|
|
2285
|
+
* - A **concise style string** (for rules applying to the root class).
|
|
2286
|
+
* - An **object** where keys are selectors (with `&` representing the root class)
|
|
2287
|
+
* and values are concise style strings or nested objects. When the key does not contain `&`,
|
|
2288
|
+
* it is treated as a descendant selector. So `{p: "color:red"}` becomes `".AbdStlX p { color: red; }"` with `AbdStlX` being the generated class name.
|
|
2085
2289
|
*
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2290
|
+
* ### Concise Style Strings
|
|
2291
|
+
*
|
|
2292
|
+
* Concise style strings use two syntaxes (same as inline CSS in {@link $}):
|
|
2293
|
+
* - **Short form** `key:value` (no space after colon): The value ends at the next whitespace.
|
|
2294
|
+
* Example: `'m:$3 bg:red r:8px'`
|
|
2295
|
+
* - **Long form** `key: value;` (space after colon): The value continues until a semicolon.
|
|
2296
|
+
* Example: `'box-shadow: 2px 0 6px black; transition: all 0.3s ease;'`
|
|
2297
|
+
*
|
|
2298
|
+
* Both forms can be mixed: `'m:$3 box-shadow: 0 2px 4px rgba(0,0,0,0.2); bg:$cardBg'`
|
|
2299
|
+
*
|
|
2300
|
+
* Supports the same CSS shortcuts as {@link $} and CSS variable references with `$` (e.g., `$primary`, `$3`).
|
|
2301
|
+
*
|
|
2302
|
+
* @param style - A concise style string or a style object.
|
|
2303
|
+
* @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`).
|
|
2304
|
+
* Use this prefix with {@link $} to apply the styles.
|
|
2095
2305
|
*
|
|
2096
|
-
* @example
|
|
2306
|
+
* @example Basic Usage with Shortcuts and CSS Variables
|
|
2097
2307
|
* ```typescript
|
|
2098
|
-
* const
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
* '
|
|
2105
|
-
*
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2308
|
+
* const cardClass = insertCss({
|
|
2309
|
+
* '&': 'bg:white p:$4 r:8px transition: background-color 0.3s;',
|
|
2310
|
+
* '&:hover': 'bg:#f5f5f5',
|
|
2311
|
+
* });
|
|
2312
|
+
*
|
|
2313
|
+
* $('section', cardClass, () => {
|
|
2314
|
+
* $('p#Card content');
|
|
2315
|
+
* });
|
|
2316
|
+
* ```
|
|
2317
|
+
*
|
|
2318
|
+
* @example Nested Selectors and Media Queries
|
|
2319
|
+
* ```typescript
|
|
2320
|
+
* const formClass = insertCss({
|
|
2321
|
+
* '&': 'bg:#0004 p:$3 r:$2',
|
|
2322
|
+
* button: {
|
|
2323
|
+
* '&': 'bg:$primary fg:white p:$2 r:4px cursor:pointer',
|
|
2324
|
+
* '&:hover': 'bg:$primaryHover',
|
|
2325
|
+
* '&:disabled': 'bg:#ccc cursor:not-allowed',
|
|
2326
|
+
* '.icon': 'display:inline-block mr:$1',
|
|
2327
|
+
* '@media (max-width: 600px)': 'p:$1 font-size:14px'
|
|
2109
2328
|
* }
|
|
2110
2329
|
* });
|
|
2111
|
-
* // scopeClass might be ".AbdStl1"
|
|
2112
2330
|
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2331
|
+
* $('form', formClass, () => {
|
|
2332
|
+
* $('button', () => {
|
|
2333
|
+
* $('span.icon text=🔥');
|
|
2334
|
+
* $('#Click Me');
|
|
2335
|
+
* });
|
|
2117
2336
|
* });
|
|
2118
2337
|
* ```
|
|
2338
|
+
*
|
|
2339
|
+
* @example Complex CSS Values
|
|
2340
|
+
* ```typescript
|
|
2341
|
+
* const badge = insertCss({
|
|
2342
|
+
* '&::before': 'content: "★"; color:gold mr:$1',
|
|
2343
|
+
* '&': 'position:relative box-shadow: 0 2px 8px rgba(0,0,0,0.15);'
|
|
2344
|
+
* });
|
|
2345
|
+
*
|
|
2346
|
+
* $(badge + ' span#Product Name');
|
|
2347
|
+
* ```
|
|
2119
2348
|
*/
|
|
2120
|
-
export function insertCss(style:
|
|
2121
|
-
const prefix =
|
|
2122
|
-
const css =
|
|
2349
|
+
export function insertCss(style: string | object): string {
|
|
2350
|
+
const prefix = `.AbdStl${++cssCount}`;
|
|
2351
|
+
const css = typeof style === 'string' ? styleStringToCss(style, prefix) : objectToCss(style, prefix);
|
|
2123
2352
|
if (css) $(`style#${css}`);
|
|
2124
2353
|
return prefix;
|
|
2125
2354
|
}
|
|
2126
2355
|
|
|
2356
|
+
function objectToCss(style: object, prefix: string): string {
|
|
2357
|
+
let css = "";
|
|
2358
|
+
|
|
2359
|
+
for (const [key, val] of Object.entries(style)) {
|
|
2360
|
+
if (val && typeof val === 'object') {
|
|
2361
|
+
// Nested object for media queries or compound selectors
|
|
2362
|
+
if (key.startsWith("@")) {
|
|
2363
|
+
// Media query or @ rule - nest the content
|
|
2364
|
+
css += `${key}{\n${objectToCss(val, prefix)}}\n`;
|
|
2365
|
+
} else {
|
|
2366
|
+
// Regular nested selector
|
|
2367
|
+
const sel = key === '&' ? prefix :
|
|
2368
|
+
key.includes("&") ? key.replace(/&/g, prefix) :
|
|
2369
|
+
`${prefix} ${key}`.trim();
|
|
2370
|
+
css += objectToCss(val, sel);
|
|
2371
|
+
}
|
|
2372
|
+
} else if (typeof val === 'string') {
|
|
2373
|
+
if (key.startsWith("@")) {
|
|
2374
|
+
// Media query with string value - wrap it
|
|
2375
|
+
css += `${key}{\n${styleStringToCss(val, prefix)}}\n`;
|
|
2376
|
+
} else {
|
|
2377
|
+
// String value - parse as style string
|
|
2378
|
+
const sel = key.includes("&") ? key.replace(/&/g, prefix) : `${prefix} ${key}`.trim();
|
|
2379
|
+
css += styleStringToCss(val, sel);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
return css;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
const KEBAB_SEGMENT = /-([a-z])/g;
|
|
2389
|
+
function toCamel(p: string) {
|
|
2390
|
+
return p.replace(KEBAB_SEGMENT, (_, l) => l.toUpperCase());
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
function styleStringToCss(styleStr: string, selector: string): string {
|
|
2394
|
+
let props = "";
|
|
2395
|
+
|
|
2396
|
+
for (let pos = 0, len = styleStr.length; pos < len;) {
|
|
2397
|
+
while (styleStr[pos] === ' ') pos++; // Skip whitespace
|
|
2398
|
+
if (pos >= len) break;
|
|
2399
|
+
|
|
2400
|
+
const colon = styleStr.indexOf(':', pos);
|
|
2401
|
+
if (colon === -1) break;
|
|
2402
|
+
const key = styleStr.substring(pos, colon);
|
|
2403
|
+
pos = colon + 1;
|
|
2404
|
+
|
|
2405
|
+
// Parse value based on whether there's a space after the colon
|
|
2406
|
+
let val: string;
|
|
2407
|
+
if (styleStr[pos] === ' ') {
|
|
2408
|
+
// Long form: space after colon means value ends at semicolon
|
|
2409
|
+
pos++; // skip the space
|
|
2410
|
+
const semi = styleStr.indexOf(';', pos);
|
|
2411
|
+
val = styleStr.substring(pos, semi === -1 ? len : semi).trim();
|
|
2412
|
+
pos = semi === -1 ? len : semi + 1;
|
|
2413
|
+
} else {
|
|
2414
|
+
// Short form: no space means value ends at whitespace
|
|
2415
|
+
const space = styleStr.indexOf(' ', pos);
|
|
2416
|
+
val = styleStr.substring(pos, space === -1 ? len : space);
|
|
2417
|
+
pos = space === -1 ? len : space;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// Expand shortcuts and add to props
|
|
2421
|
+
const v = cssVarRef(val);
|
|
2422
|
+
const exp = CSS_SHORT[key] || key;
|
|
2423
|
+
props += typeof exp === 'string' ? `${exp}:${v};` : exp.map(p => `${p}:${v};`).join('');
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
return props ? `${selector}{${props}}\n` : "";
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2127
2429
|
/**
|
|
2128
|
-
* Inserts CSS rules globally.
|
|
2430
|
+
* Inserts CSS rules globally (unscoped).
|
|
2129
2431
|
*
|
|
2130
2432
|
* Works exactly like {@link insertCss}, but without prefixing selectors with a unique class name.
|
|
2433
|
+
* This is useful for global resets, base styles, or styles that need to apply to the entire document.
|
|
2434
|
+
*
|
|
2435
|
+
* Accepts the same concise style string syntax and CSS shortcuts as {@link insertCss}.
|
|
2436
|
+
* See {@link insertCss} for detailed documentation on syntax and shortcuts.
|
|
2437
|
+
*
|
|
2438
|
+
* @param style - Object with selectors as keys and concise CSS strings as values.
|
|
2131
2439
|
*
|
|
2132
|
-
* @example Global Styles
|
|
2440
|
+
* @example Global Reset and Base Styles
|
|
2441
|
+
* ```typescript
|
|
2442
|
+
* // Set up global styles using CSS shortcuts
|
|
2443
|
+
* insertGlobalCss({
|
|
2444
|
+
* "*": "m:0 p:0 box-sizing:border-box",
|
|
2445
|
+
* "body": "font-family: system-ui, sans-serif; m:0 p:$3 bg:#434 fg:#d0dafa",
|
|
2446
|
+
* "a": "text-decoration:none fg:#57f",
|
|
2447
|
+
* "a:hover": "text-decoration:underline",
|
|
2448
|
+
* "code": "font-family:monospace bg:#222 fg:#afc p:4px r:3px"
|
|
2449
|
+
* });
|
|
2450
|
+
*
|
|
2451
|
+
* $('h2#Title without margins');
|
|
2452
|
+
* $('a#This is a link');
|
|
2453
|
+
* $('code#const x = 42;');
|
|
2454
|
+
* ```
|
|
2455
|
+
*
|
|
2456
|
+
* @example Responsive Global Styles
|
|
2133
2457
|
* ```typescript
|
|
2134
2458
|
* insertGlobalCss({
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2459
|
+
* "html": "font-size:16px",
|
|
2460
|
+
* "body": "line-height:1.6",
|
|
2461
|
+
* "h1, h2, h3": "font-weight:600 mt:$4 mb:$2",
|
|
2462
|
+
* "@media (max-width: 768px)": {
|
|
2463
|
+
* "html": "font-size:14px",
|
|
2464
|
+
* "body": "p:$2"
|
|
2138
2465
|
* },
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2466
|
+
* "@media (prefers-color-scheme: dark)": {
|
|
2467
|
+
* "body": "bg:#1a1a1a fg:#e5e5e5",
|
|
2468
|
+
* "code": "bg:#2a2a2a"
|
|
2142
2469
|
* }
|
|
2143
2470
|
* });
|
|
2144
|
-
*
|
|
2145
|
-
* $('a#Styled link');
|
|
2146
2471
|
* ```
|
|
2147
2472
|
*/
|
|
2148
|
-
export function insertGlobalCss(style: object)
|
|
2149
|
-
|
|
2473
|
+
export function insertGlobalCss(style: object) {
|
|
2474
|
+
const css = objectToCss(style, "");
|
|
2475
|
+
if (css) $(`style#${css}`);
|
|
2150
2476
|
}
|
|
2151
2477
|
|
|
2152
2478
|
const CSS_SHORT: Record<string, string | string[]> = {
|
|
2153
2479
|
m: "margin",
|
|
2154
|
-
mt: "
|
|
2155
|
-
mb: "
|
|
2156
|
-
ml: "
|
|
2157
|
-
mr: "
|
|
2158
|
-
mh: ["
|
|
2159
|
-
mv: ["
|
|
2480
|
+
mt: "margin-top",
|
|
2481
|
+
mb: "margin-bottom",
|
|
2482
|
+
ml: "margin-left",
|
|
2483
|
+
mr: "margin-right",
|
|
2484
|
+
mh: ["margin-left", "margin-right"],
|
|
2485
|
+
mv: ["margin-top", "margin-bottom"],
|
|
2160
2486
|
p: "padding",
|
|
2161
|
-
pt: "
|
|
2162
|
-
pb: "
|
|
2163
|
-
pl: "
|
|
2164
|
-
pr: "
|
|
2165
|
-
ph: ["
|
|
2166
|
-
pv: ["
|
|
2487
|
+
pt: "padding-top",
|
|
2488
|
+
pb: "padding-bottom",
|
|
2489
|
+
pl: "padding-left",
|
|
2490
|
+
pr: "padding-right",
|
|
2491
|
+
ph: ["padding-left", "padding-right"],
|
|
2492
|
+
pv: ["padding-top", "padding-bottom"],
|
|
2167
2493
|
w: "width",
|
|
2168
2494
|
h: "height",
|
|
2169
2495
|
bg: "background",
|
|
2170
2496
|
fg: "color",
|
|
2171
|
-
r: "
|
|
2497
|
+
r: "border-radius",
|
|
2172
2498
|
};
|
|
2173
2499
|
|
|
2174
|
-
function styleToCss(style: object, prefix: string): string {
|
|
2175
|
-
let props = "";
|
|
2176
|
-
let rules = "";
|
|
2177
|
-
for (const kOr of Object.keys(style)) {
|
|
2178
|
-
const v = (style as any)[kOr];
|
|
2179
|
-
for (const k of kOr.split(/, ?/g)) {
|
|
2180
|
-
if (v && typeof v === "object") {
|
|
2181
|
-
if (k.startsWith("@")) {
|
|
2182
|
-
// media queries
|
|
2183
|
-
rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
|
|
2184
|
-
} else {
|
|
2185
|
-
rules += styleToCss(
|
|
2186
|
-
v,
|
|
2187
|
-
k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
|
|
2188
|
-
);
|
|
2189
|
-
}
|
|
2190
|
-
} else {
|
|
2191
|
-
const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '@' ? cssVarRef(v.substring(1)) : v) : String(v);
|
|
2192
|
-
const expanded = CSS_SHORT[k] || k;
|
|
2193
|
-
for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
|
|
2194
|
-
props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
|
|
2200
|
-
return rules;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
2500
|
function applyArg(el: Element, key: string, value: any) {
|
|
2204
2501
|
if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
|
|
2205
2502
|
// Value is a proxy
|
|
@@ -2217,12 +2514,12 @@ function applyArg(el: Element, key: string, value: any) {
|
|
|
2217
2514
|
} else if (key[0] === "$") {
|
|
2218
2515
|
// Style (with shortcuts)
|
|
2219
2516
|
key = key.substring(1);
|
|
2220
|
-
const val = value == null || value === false ? "" : typeof value === 'string' ?
|
|
2517
|
+
const val = value == null || value === false ? "" : typeof value === 'string' ? cssVarRef(value) : String(value);
|
|
2221
2518
|
const expanded = CSS_SHORT[key] || key;
|
|
2222
2519
|
if (typeof expanded === "string") {
|
|
2223
|
-
(el as any).style[expanded] = val;
|
|
2520
|
+
(el as any).style[toCamel(expanded)] = val;
|
|
2224
2521
|
} else {
|
|
2225
|
-
for (const prop of expanded) (el as any).style[prop] = val;
|
|
2522
|
+
for (const prop of expanded) (el as any).style[toCamel(prop)] = val;
|
|
2226
2523
|
}
|
|
2227
2524
|
} else if (value == null) {
|
|
2228
2525
|
// Value left empty
|
|
@@ -2278,7 +2575,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2278
2575
|
*
|
|
2279
2576
|
* try {
|
|
2280
2577
|
* // Attempt to show a custom message in the UI
|
|
2281
|
-
* $('div
|
|
2578
|
+
* $('div#Oops, something went wrong!', errorClass);
|
|
2282
2579
|
* } catch (e) {
|
|
2283
2580
|
* // Ignore errors during error handling itself
|
|
2284
2581
|
* }
|
|
@@ -2287,15 +2584,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2287
2584
|
* });
|
|
2288
2585
|
*
|
|
2289
2586
|
* // Styling for our custom error message
|
|
2290
|
-
* insertCss(
|
|
2291
|
-
* '.error-message': {
|
|
2292
|
-
* backgroundColor: '#e31f00',
|
|
2293
|
-
* display: 'inline-block',
|
|
2294
|
-
* color: 'white',
|
|
2295
|
-
* borderRadius: '3px',
|
|
2296
|
-
* padding: '2px 4px',
|
|
2297
|
-
* }
|
|
2298
|
-
* }, true); // global style
|
|
2587
|
+
* const errorClass = insertCss('background-color:#e31f00 display:inline-block color:white r:3px padding: 2px 4px;');
|
|
2299
2588
|
*
|
|
2300
2589
|
* // Cause an error within a render scope.
|
|
2301
2590
|
* $('div.box', () => {
|
|
@@ -2310,34 +2599,6 @@ export function setErrorHandler(
|
|
|
2310
2599
|
onError = handler || defaultOnError;
|
|
2311
2600
|
}
|
|
2312
2601
|
|
|
2313
|
-
/**
|
|
2314
|
-
* Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
|
|
2315
|
-
*
|
|
2316
|
-
* This is context-dependent based on the current reactive scope (e.g., inside a {@link mount}
|
|
2317
|
-
* call or a {@link $} element's render function).
|
|
2318
|
-
*
|
|
2319
|
-
* **Note:** While this provides access to the DOM element, directly manipulating it outside
|
|
2320
|
-
* of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
|
|
2321
|
-
*
|
|
2322
|
-
* @returns The current parent `Element` for DOM insertion.
|
|
2323
|
-
*
|
|
2324
|
-
* @example Get parent for attaching a third-party library
|
|
2325
|
-
* ```typescript
|
|
2326
|
-
* function thirdPartyLibInit(parentElement) {
|
|
2327
|
-
* parentElement.innerHTML = "This element is managed by a <em>third party</em> lib."
|
|
2328
|
-
* }
|
|
2329
|
-
*
|
|
2330
|
-
* $('div.box', () => {
|
|
2331
|
-
* // Get the div.box element just created
|
|
2332
|
-
* const containerElement = getParentElement();
|
|
2333
|
-
* thirdPartyLibInit(containerElement);
|
|
2334
|
-
* });
|
|
2335
|
-
* ```
|
|
2336
|
-
*/
|
|
2337
|
-
export function getParentElement(): Element {
|
|
2338
|
-
return currentScope.el;
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
2602
|
/**
|
|
2342
2603
|
* Registers a cleanup function to be executed just before the current reactive scope
|
|
2343
2604
|
* is destroyed or redraws.
|
|
@@ -2483,7 +2744,8 @@ export function mount(parentElement: Element, func: () => void) {
|
|
|
2483
2744
|
* Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
|
|
2484
2745
|
* (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
|
|
2485
2746
|
*
|
|
2486
|
-
* This effectively cleans up the entire Aberdeen application state.
|
|
2747
|
+
* This effectively cleans up the entire Aberdeen application state. Aside from in
|
|
2748
|
+
* automated tests, there should probably be little reason to call this function.
|
|
2487
2749
|
*/
|
|
2488
2750
|
export function unmountAll() {
|
|
2489
2751
|
ROOT_SCOPE.remove();
|
|
@@ -2604,7 +2866,7 @@ export function map(
|
|
|
2604
2866
|
} else {
|
|
2605
2867
|
out = optProxy({});
|
|
2606
2868
|
}
|
|
2607
|
-
|
|
2869
|
+
|
|
2608
2870
|
onEach(source, (item: any, key: symbol | string | number) => {
|
|
2609
2871
|
const value = func(item, key);
|
|
2610
2872
|
if (value !== undefined) {
|
|
@@ -2914,20 +3176,4 @@ export function withEmitHandler(
|
|
|
2914
3176
|
}
|
|
2915
3177
|
}
|
|
2916
3178
|
|
|
2917
|
-
|
|
2918
|
-
// This runs at module load time, after all functions are defined
|
|
2919
|
-
if (typeof document !== "undefined") {
|
|
2920
|
-
leakScope(() => {
|
|
2921
|
-
mount(document.head, () => {
|
|
2922
|
-
$('style', () => {
|
|
2923
|
-
let css = ":root {\n";
|
|
2924
|
-
for(const [key, value] of Object.entries(cssVars)) {
|
|
2925
|
-
const varName = DIGIT_FIRST.test(String(key)) ? `m${key}` : key;
|
|
2926
|
-
css += ` --${varName}: ${value};\n`;
|
|
2927
|
-
}
|
|
2928
|
-
css += "}";
|
|
2929
|
-
$(`#${css}`);
|
|
2930
|
-
})
|
|
2931
|
-
});
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
3179
|
+
|