aberdeen 1.6.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 +173 -129
- package/dist/aberdeen.js +181 -95
- 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 +62 -20
- 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 +1 -1
- package/skill/SKILL.md +286 -76
- package/skill/aberdeen.md +219 -203
- package/skill/dispatcher.md +16 -13
- package/skill/prediction.md +3 -3
- package/skill/route.md +44 -16
- package/skill/transitions.md +3 -3
- package/src/aberdeen.ts +397 -237
- package/src/dispatcher.ts +16 -13
- package/src/route.ts +90 -19
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;
|
|
@@ -1737,15 +1772,16 @@ export const NO_COPY = Symbol("NO_COPY");
|
|
|
1737
1772
|
export const cssVars: Record<string, string> = optProxy({});
|
|
1738
1773
|
|
|
1739
1774
|
/**
|
|
1740
|
-
* Initializes `cssVars[
|
|
1775
|
+
* Initializes `cssVars[0]` through `cssVars[12]` with an exponential spacing scale.
|
|
1741
1776
|
*
|
|
1742
1777
|
* The scale is calculated as `2^(n-3) * base`, providing values from `0.25 * base` to `512 * base`.
|
|
1743
1778
|
*
|
|
1744
|
-
* @param base - The base size for the spacing scale
|
|
1745
|
-
* @param unit - The CSS unit to use
|
|
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'.
|
|
1746
1781
|
*
|
|
1747
1782
|
* @example
|
|
1748
1783
|
* ```javascript
|
|
1784
|
+
* import { setSpacingCssVars, cssVars, onEach, $} from 'aberdeen';
|
|
1749
1785
|
* // Use default scale (0.25rem to 512rem)
|
|
1750
1786
|
* setSpacingCssVars();
|
|
1751
1787
|
*
|
|
@@ -1754,19 +1790,36 @@ export const cssVars: Record<string, string> = optProxy({});
|
|
|
1754
1790
|
*
|
|
1755
1791
|
* // Use em units
|
|
1756
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
|
|
1757
1798
|
* ```
|
|
1758
1799
|
*/
|
|
1759
1800
|
export function setSpacingCssVars(base = 1, unit = 'rem'): void {
|
|
1760
|
-
for (let i =
|
|
1801
|
+
for (let i = 0; i <= 12; i++) {
|
|
1761
1802
|
cssVars[i] = 2 ** (i - 3) * base + unit;
|
|
1762
1803
|
}
|
|
1763
1804
|
}
|
|
1764
1805
|
|
|
1806
|
+
// Matches: (1) parenthesized content, (2) quoted content, (3) $varName at start or after space
|
|
1807
|
+
const CSS_VAR_PATTERN = /(\([^)]*\))|("[^"]*")|(^| )\$(\w+)/g;
|
|
1765
1808
|
const DIGIT_FIRST = /^\d/;
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
+
});
|
|
1770
1823
|
}
|
|
1771
1824
|
|
|
1772
1825
|
// Automatically mount cssVars style tag to document.head when cssVars is not empty
|
|
@@ -1790,6 +1843,9 @@ if (typeof document !== "undefined") {
|
|
|
1790
1843
|
});
|
|
1791
1844
|
}
|
|
1792
1845
|
|
|
1846
|
+
|
|
1847
|
+
let darkModeState: {value: boolean} | undefined;
|
|
1848
|
+
|
|
1793
1849
|
/**
|
|
1794
1850
|
* Returns whether the user's browser prefers a dark color scheme.
|
|
1795
1851
|
*
|
|
@@ -1806,33 +1862,25 @@ if (typeof document !== "undefined") {
|
|
|
1806
1862
|
*
|
|
1807
1863
|
* // Reactively set colors based on browser preference
|
|
1808
1864
|
* $(() => {
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
* cssVars.fg = '#e5e5e5';
|
|
1812
|
-
* } else {
|
|
1813
|
-
* cssVars.bg = '#ffffff';
|
|
1814
|
-
* cssVars.fg = '#000000';
|
|
1815
|
-
* }
|
|
1865
|
+
* cssVars.bg = darkMode() ? '#1a1a1a' : '#ffffff';
|
|
1866
|
+
* cssVars.fg = darkMode() ? '#e5e5e5' : '#000000';
|
|
1816
1867
|
* });
|
|
1868
|
+
*
|
|
1869
|
+
* $('div bg:$bg fg:$fg p:1rem #Colors change based on system dark mode preference');
|
|
1817
1870
|
* ```
|
|
1818
1871
|
*/
|
|
1819
1872
|
export function darkMode(): boolean {
|
|
1820
|
-
if (
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
changed.value = true;
|
|
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);
|
|
1829
1881
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
mediaQuery.removeEventListener('change', onChange);
|
|
1833
|
-
})
|
|
1834
|
-
|
|
1835
|
-
return mediaQuery.matches;
|
|
1882
|
+
|
|
1883
|
+
return darkModeState.value;
|
|
1836
1884
|
}
|
|
1837
1885
|
|
|
1838
1886
|
/**
|
|
@@ -1935,12 +1983,9 @@ function applyBind(el: HTMLInputElement, target: any) {
|
|
|
1935
1983
|
};
|
|
1936
1984
|
} else {
|
|
1937
1985
|
onInputChange = () => {
|
|
1938
|
-
target.value =
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
? null
|
|
1942
|
-
: +el.value
|
|
1943
|
-
: el.value;
|
|
1986
|
+
target.value = type === "number" || type === "range"
|
|
1987
|
+
? el.value === "" ? null : +el.value
|
|
1988
|
+
: el.value;
|
|
1944
1989
|
};
|
|
1945
1990
|
if (value === undefined) onInputChange();
|
|
1946
1991
|
onProxyChange = () => {
|
|
@@ -1996,38 +2041,62 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
|
|
|
1996
2041
|
*
|
|
1997
2042
|
* @param {...(string | function | object | false | undefined | null)} args - Any number of arguments can be given. How they're interpreted depends on their types:
|
|
1998
2043
|
*
|
|
1999
|
-
*
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
* -
|
|
2008
|
-
*
|
|
2009
|
-
* -
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
*
|
|
2026
|
-
*
|
|
2027
|
-
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
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.
|
|
2031
2100
|
*
|
|
2032
2101
|
* @example Create Element
|
|
2033
2102
|
* ```typescript
|
|
@@ -2074,6 +2143,14 @@ const SPECIAL_PROPS: { [key: string]: (el: Element, value: any) => void } = {
|
|
|
2074
2143
|
* }
|
|
2075
2144
|
* });
|
|
2076
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`.
|
|
2077
2154
|
*/
|
|
2078
2155
|
|
|
2079
2156
|
export function $(...args: any[]): undefined | Element {
|
|
@@ -2092,16 +2169,37 @@ export function $(...args: any[]): undefined | Element {
|
|
|
2092
2169
|
nextPos = findFirst(arg, " .=:#", pos);
|
|
2093
2170
|
const next = arg[nextPos];
|
|
2094
2171
|
|
|
2095
|
-
if (next === ":"
|
|
2096
|
-
|
|
2097
|
-
|
|
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);
|
|
2098
2195
|
if (nextPos + 1 >= argLen) {
|
|
2099
2196
|
applyArg(el, key, args[++argIndex]);
|
|
2100
2197
|
break;
|
|
2101
2198
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
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);
|
|
2105
2203
|
applyArg(el, key, value);
|
|
2106
2204
|
nextPos = endIndex;
|
|
2107
2205
|
} else {
|
|
@@ -2133,6 +2231,7 @@ export function $(...args: any[]): undefined | Element {
|
|
|
2133
2231
|
applyArg(el, arg.substring(nextPos, classEnd), args[++argIndex]);
|
|
2134
2232
|
nextPos = classEnd;
|
|
2135
2233
|
} else {
|
|
2234
|
+
// An unconditional class name
|
|
2136
2235
|
let className: any = arg.substring(nextPos + 1, classEnd);
|
|
2137
2236
|
el.classList.add(className || args[++argIndex]);
|
|
2138
2237
|
nextPos = classEnd - 1;
|
|
@@ -2182,126 +2281,222 @@ let cssCount = 0;
|
|
|
2182
2281
|
/**
|
|
2183
2282
|
* Inserts CSS rules into the document, scoping them with a unique class name.
|
|
2184
2283
|
*
|
|
2185
|
-
*
|
|
2186
|
-
*
|
|
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.
|
|
2289
|
+
*
|
|
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.
|
|
2305
|
+
*
|
|
2306
|
+
* @example Basic Usage with Shortcuts and CSS Variables
|
|
2307
|
+
* ```typescript
|
|
2308
|
+
* const cardClass = insertCss({
|
|
2309
|
+
* '&': 'bg:white p:$4 r:8px transition: background-color 0.3s;',
|
|
2310
|
+
* '&:hover': 'bg:#f5f5f5',
|
|
2311
|
+
* });
|
|
2187
2312
|
*
|
|
2188
|
-
*
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
*
|
|
2192
|
-
* - Selectors will be split on `,` characters, each combining with the parent selector with *or* semantics.
|
|
2193
|
-
* - Selector starting with `'@'` define at-rules like media queries. They may be nested within regular selectors.
|
|
2194
|
-
* @param global - Deprecated! Use {@link insertGlobalCss} instead.
|
|
2195
|
-
* @returns The unique class name prefix used for scoping (e.g., `.AbdStl1`). Use this
|
|
2196
|
-
* prefix with {@link $} to apply the styles.
|
|
2313
|
+
* $('section', cardClass, () => {
|
|
2314
|
+
* $('p#Card content');
|
|
2315
|
+
* });
|
|
2316
|
+
* ```
|
|
2197
2317
|
*
|
|
2198
|
-
* @example
|
|
2318
|
+
* @example Nested Selectors and Media Queries
|
|
2199
2319
|
* ```typescript
|
|
2200
|
-
* const
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
* },
|
|
2209
|
-
* '@media (max-width: 600px)': {
|
|
2210
|
-
* padding: '5px'
|
|
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'
|
|
2211
2328
|
* }
|
|
2212
2329
|
* });
|
|
2213
|
-
* // scopeClass might be ".AbdStl1"
|
|
2214
2330
|
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
2331
|
+
* $('form', formClass, () => {
|
|
2332
|
+
* $('button', () => {
|
|
2333
|
+
* $('span.icon text=🔥');
|
|
2334
|
+
* $('#Click Me');
|
|
2335
|
+
* });
|
|
2219
2336
|
* });
|
|
2220
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
|
+
* ```
|
|
2221
2348
|
*/
|
|
2222
|
-
export function insertCss(style:
|
|
2223
|
-
const prefix =
|
|
2224
|
-
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);
|
|
2225
2352
|
if (css) $(`style#${css}`);
|
|
2226
2353
|
return prefix;
|
|
2227
2354
|
}
|
|
2228
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
|
+
|
|
2229
2429
|
/**
|
|
2230
|
-
* Inserts CSS rules globally.
|
|
2430
|
+
* Inserts CSS rules globally (unscoped).
|
|
2231
2431
|
*
|
|
2232
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.
|
|
2233
2434
|
*
|
|
2234
|
-
* @
|
|
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.
|
|
2439
|
+
*
|
|
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
|
|
2235
2457
|
* ```typescript
|
|
2236
2458
|
* insertGlobalCss({
|
|
2237
|
-
*
|
|
2238
|
-
*
|
|
2239
|
-
*
|
|
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"
|
|
2240
2465
|
* },
|
|
2241
|
-
*
|
|
2242
|
-
*
|
|
2243
|
-
*
|
|
2466
|
+
* "@media (prefers-color-scheme: dark)": {
|
|
2467
|
+
* "body": "bg:#1a1a1a fg:#e5e5e5",
|
|
2468
|
+
* "code": "bg:#2a2a2a"
|
|
2244
2469
|
* }
|
|
2245
2470
|
* });
|
|
2246
|
-
*
|
|
2247
|
-
* $('a#Styled link');
|
|
2248
2471
|
* ```
|
|
2249
2472
|
*/
|
|
2250
|
-
export function insertGlobalCss(style: object)
|
|
2251
|
-
|
|
2473
|
+
export function insertGlobalCss(style: object) {
|
|
2474
|
+
const css = objectToCss(style, "");
|
|
2475
|
+
if (css) $(`style#${css}`);
|
|
2252
2476
|
}
|
|
2253
2477
|
|
|
2254
2478
|
const CSS_SHORT: Record<string, string | string[]> = {
|
|
2255
2479
|
m: "margin",
|
|
2256
|
-
mt: "
|
|
2257
|
-
mb: "
|
|
2258
|
-
ml: "
|
|
2259
|
-
mr: "
|
|
2260
|
-
mh: ["
|
|
2261
|
-
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"],
|
|
2262
2486
|
p: "padding",
|
|
2263
|
-
pt: "
|
|
2264
|
-
pb: "
|
|
2265
|
-
pl: "
|
|
2266
|
-
pr: "
|
|
2267
|
-
ph: ["
|
|
2268
|
-
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"],
|
|
2269
2493
|
w: "width",
|
|
2270
2494
|
h: "height",
|
|
2271
2495
|
bg: "background",
|
|
2272
2496
|
fg: "color",
|
|
2273
|
-
r: "
|
|
2497
|
+
r: "border-radius",
|
|
2274
2498
|
};
|
|
2275
2499
|
|
|
2276
|
-
function styleToCss(style: object, prefix: string): string {
|
|
2277
|
-
let props = "";
|
|
2278
|
-
let rules = "";
|
|
2279
|
-
for (const kOr of Object.keys(style)) {
|
|
2280
|
-
const v = (style as any)[kOr];
|
|
2281
|
-
for (const k of kOr.split(/, ?/g)) {
|
|
2282
|
-
if (v && typeof v === "object") {
|
|
2283
|
-
if (k.startsWith("@")) {
|
|
2284
|
-
// media queries
|
|
2285
|
-
rules += `${k}{\n${styleToCss(v, prefix)}}\n`;
|
|
2286
|
-
} else {
|
|
2287
|
-
rules += styleToCss(
|
|
2288
|
-
v,
|
|
2289
|
-
k.includes("&") ? k.replace(/&/g, prefix) : `${prefix} ${k}`,
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
} else {
|
|
2293
|
-
const val = v == null || v === false ? "" : typeof v === 'string' ? (v[0] === '$' ? cssVarRef(v.substring(1)) : v) : String(v);
|
|
2294
|
-
const expanded = CSS_SHORT[k] || k;
|
|
2295
|
-
for (const prop of (Array.isArray(expanded) ? expanded : [expanded])) {
|
|
2296
|
-
props += `${prop.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}:${val};`;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
if (props) rules = `${prefix.trimStart() || "*"}{${props}}\n${rules}`;
|
|
2302
|
-
return rules;
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
2500
|
function applyArg(el: Element, key: string, value: any) {
|
|
2306
2501
|
if (typeof value === "object" && value !== null && value[TARGET_SYMBOL]) {
|
|
2307
2502
|
// Value is a proxy
|
|
@@ -2319,12 +2514,12 @@ function applyArg(el: Element, key: string, value: any) {
|
|
|
2319
2514
|
} else if (key[0] === "$") {
|
|
2320
2515
|
// Style (with shortcuts)
|
|
2321
2516
|
key = key.substring(1);
|
|
2322
|
-
const val = value == null || value === false ? "" : typeof value === 'string' ?
|
|
2517
|
+
const val = value == null || value === false ? "" : typeof value === 'string' ? cssVarRef(value) : String(value);
|
|
2323
2518
|
const expanded = CSS_SHORT[key] || key;
|
|
2324
2519
|
if (typeof expanded === "string") {
|
|
2325
|
-
(el as any).style[expanded] = val;
|
|
2520
|
+
(el as any).style[toCamel(expanded)] = val;
|
|
2326
2521
|
} else {
|
|
2327
|
-
for (const prop of expanded) (el as any).style[prop] = val;
|
|
2522
|
+
for (const prop of expanded) (el as any).style[toCamel(prop)] = val;
|
|
2328
2523
|
}
|
|
2329
2524
|
} else if (value == null) {
|
|
2330
2525
|
// Value left empty
|
|
@@ -2380,7 +2575,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2380
2575
|
*
|
|
2381
2576
|
* try {
|
|
2382
2577
|
* // Attempt to show a custom message in the UI
|
|
2383
|
-
* $('div
|
|
2578
|
+
* $('div#Oops, something went wrong!', errorClass);
|
|
2384
2579
|
* } catch (e) {
|
|
2385
2580
|
* // Ignore errors during error handling itself
|
|
2386
2581
|
* }
|
|
@@ -2389,15 +2584,7 @@ let onError: (error: Error) => boolean | undefined = defaultOnError;
|
|
|
2389
2584
|
* });
|
|
2390
2585
|
*
|
|
2391
2586
|
* // Styling for our custom error message
|
|
2392
|
-
* insertCss(
|
|
2393
|
-
* '.error-message': {
|
|
2394
|
-
* backgroundColor: '#e31f00',
|
|
2395
|
-
* display: 'inline-block',
|
|
2396
|
-
* color: 'white',
|
|
2397
|
-
* borderRadius: '3px',
|
|
2398
|
-
* padding: '2px 4px',
|
|
2399
|
-
* }
|
|
2400
|
-
* }, true); // global style
|
|
2587
|
+
* const errorClass = insertCss('background-color:#e31f00 display:inline-block color:white r:3px padding: 2px 4px;');
|
|
2401
2588
|
*
|
|
2402
2589
|
* // Cause an error within a render scope.
|
|
2403
2590
|
* $('div.box', () => {
|
|
@@ -2412,34 +2599,6 @@ export function setErrorHandler(
|
|
|
2412
2599
|
onError = handler || defaultOnError;
|
|
2413
2600
|
}
|
|
2414
2601
|
|
|
2415
|
-
/**
|
|
2416
|
-
* Gets the parent DOM `Element` where nodes created by {@link $} would currently be inserted.
|
|
2417
|
-
*
|
|
2418
|
-
* This is context-dependent based on the current reactive scope (e.g., inside a {@link mount}
|
|
2419
|
-
* call or a {@link $} element's render function).
|
|
2420
|
-
*
|
|
2421
|
-
* **Note:** While this provides access to the DOM element, directly manipulating it outside
|
|
2422
|
-
* of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
|
|
2423
|
-
*
|
|
2424
|
-
* @returns The current parent `Element` for DOM insertion.
|
|
2425
|
-
*
|
|
2426
|
-
* @example Get parent for attaching a third-party library
|
|
2427
|
-
* ```typescript
|
|
2428
|
-
* function thirdPartyLibInit(parentElement) {
|
|
2429
|
-
* parentElement.innerHTML = "This element is managed by a <em>third party</em> lib."
|
|
2430
|
-
* }
|
|
2431
|
-
*
|
|
2432
|
-
* $('div.box', () => {
|
|
2433
|
-
* // Get the div.box element just created
|
|
2434
|
-
* const containerElement = getParentElement();
|
|
2435
|
-
* thirdPartyLibInit(containerElement);
|
|
2436
|
-
* });
|
|
2437
|
-
* ```
|
|
2438
|
-
*/
|
|
2439
|
-
export function getParentElement(): Element {
|
|
2440
|
-
return currentScope.el;
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
2602
|
/**
|
|
2444
2603
|
* Registers a cleanup function to be executed just before the current reactive scope
|
|
2445
2604
|
* is destroyed or redraws.
|
|
@@ -2585,7 +2744,8 @@ export function mount(parentElement: Element, func: () => void) {
|
|
|
2585
2744
|
* Removes all Aberdeen-managed DOM nodes and stops all active reactive scopes
|
|
2586
2745
|
* (created by {@link mount}, {@link derive}, {@link $} with functions, etc.).
|
|
2587
2746
|
*
|
|
2588
|
-
* 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.
|
|
2589
2749
|
*/
|
|
2590
2750
|
export function unmountAll() {
|
|
2591
2751
|
ROOT_SCOPE.remove();
|
|
@@ -2706,7 +2866,7 @@ export function map(
|
|
|
2706
2866
|
} else {
|
|
2707
2867
|
out = optProxy({});
|
|
2708
2868
|
}
|
|
2709
|
-
|
|
2869
|
+
|
|
2710
2870
|
onEach(source, (item: any, key: symbol | string | number) => {
|
|
2711
2871
|
const value = func(item, key);
|
|
2712
2872
|
if (value !== undefined) {
|