@oscarpalmer/abydon 0.21.0 → 0.22.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 +2 -0
- package/dist/abydon.full.mjs +247 -246
- package/dist/attribute/index.d.mts +6 -0
- package/dist/{node/attribute → attribute}/index.mjs +10 -19
- package/dist/{node/attribute → attribute}/value.d.mts +2 -2
- package/dist/attribute/value.mjs +63 -0
- package/dist/constants.d.mts +7 -3
- package/dist/constants.mjs +12 -8
- package/dist/fragment.d.mts +4 -0
- package/dist/fragment.mjs +9 -3
- package/dist/fragments.d.mts +3 -6
- package/dist/fragments.mjs +5 -11
- package/dist/helpers/index.d.mts +11 -1
- package/dist/helpers/index.mjs +10 -0
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +8 -4
- package/dist/node/event.mjs +9 -4
- package/dist/node/index.mjs +19 -10
- package/dist/node/value.mjs +9 -12
- package/dist/parse.mjs +6 -4
- package/package.json +6 -5
- package/src/{node/attribute → attribute}/index.ts +16 -33
- package/src/attribute/value.ts +140 -0
- package/src/constants.ts +16 -8
- package/src/fragment.ts +10 -5
- package/src/fragments.ts +4 -13
- package/src/helpers/index.ts +10 -0
- package/src/index.ts +16 -3
- package/src/node/event.ts +12 -5
- package/src/node/index.ts +31 -13
- package/src/node/value.ts +10 -25
- package/src/parse.ts +14 -4
- package/dist/node/attribute/index.d.mts +0 -6
- package/dist/node/attribute/value.mjs +0 -65
- package/src/node/attribute/value.ts +0 -158
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oscarpalmer/abydon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"components",
|
|
6
6
|
"dom",
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
"test:leak": "npx vp test run --detect-async-leaks --coverage"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oscarpalmer/atoms": "^0.
|
|
46
|
-
"@oscarpalmer/mora": "^0.
|
|
47
|
-
"@oscarpalmer/toretto": "^0.
|
|
45
|
+
"@oscarpalmer/atoms": "^0.185",
|
|
46
|
+
"@oscarpalmer/mora": "^0.29",
|
|
47
|
+
"@oscarpalmer/toretto": "^0.43"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@oscarpalmer/oui": "^0.20",
|
|
51
|
+
"@oxlint/plugins": "^1.62",
|
|
51
52
|
"@types/node": "^25.6",
|
|
52
53
|
"@vitest/coverage-istanbul": "^4.1",
|
|
53
|
-
"jsdom": "^29",
|
|
54
|
+
"jsdom": "^29.1",
|
|
54
55
|
"tsdown": "^0.21",
|
|
55
56
|
"typescript": "^5.9",
|
|
56
57
|
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {
|
|
3
|
-
import {computed, isReactive, isSignal} from '@oscarpalmer/mora';
|
|
2
|
+
import {computed, isSignal} from '@oscarpalmer/mora';
|
|
4
3
|
import {
|
|
5
|
-
|
|
4
|
+
EVENT_ON_VALUE,
|
|
6
5
|
EXPRESSION_ABYDON_ATTRIBUTE_PREFIX,
|
|
7
6
|
EXPRESSION_ABYDON_CONTENT,
|
|
8
7
|
EXPRESSION_EVENT_PREFIX,
|
|
9
|
-
EXPRESSION_PERIOD,
|
|
10
8
|
INPUT_TYPE_CHECKBOX,
|
|
9
|
+
INPUT_TYPE_RADIO,
|
|
11
10
|
PROPERTY_CHECKED,
|
|
12
11
|
PROPERTY_VALUE,
|
|
13
|
-
} from '
|
|
14
|
-
import {isInputElement} from '
|
|
15
|
-
import type {FragmentData} from '
|
|
16
|
-
import {mapEvent} from '../event';
|
|
12
|
+
} from '../constants';
|
|
13
|
+
import {isInputElement} from '../helpers/dom';
|
|
14
|
+
import type {FragmentData} from '../models';
|
|
15
|
+
import {mapEvent} from '../node/event';
|
|
17
16
|
import {setAttribute} from './value';
|
|
18
17
|
|
|
19
18
|
function getValue(data: FragmentData, original: string): unknown {
|
|
20
|
-
const matches = EXPRESSION_ABYDON_CONTENT.exec(original
|
|
19
|
+
const matches = EXPRESSION_ABYDON_CONTENT.exec(original);
|
|
21
20
|
|
|
22
21
|
return matches == null ? original : data.values[+matches[1]];
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export function mapAttributes(data: FragmentData, element: HTMLElement | SVGElement): void {
|
|
26
|
-
const attributes = [...element.attributes]
|
|
25
|
+
const attributes = [...element.attributes].sort((first, second) =>
|
|
26
|
+
first.name.localeCompare(second.name),
|
|
27
|
+
);
|
|
28
|
+
|
|
27
29
|
const {length} = attributes;
|
|
28
30
|
|
|
29
31
|
for (let index = 0; index < length; index += 1) {
|
|
@@ -41,13 +43,8 @@ export function mapAttributes(data: FragmentData, element: HTMLElement | SVGElem
|
|
|
41
43
|
mapEvent(element, actualName, actualValue);
|
|
42
44
|
break;
|
|
43
45
|
|
|
44
|
-
case EXPRESSION_PERIOD.test(actualName):
|
|
45
|
-
case typeof actualValue === 'function':
|
|
46
|
-
case isReactive(actualValue):
|
|
47
|
-
mapValue(data, element, actualName, actualValue);
|
|
48
|
-
break;
|
|
49
|
-
|
|
50
46
|
default:
|
|
47
|
+
mapValue(data, element, actualName, actualValue);
|
|
51
48
|
break;
|
|
52
49
|
}
|
|
53
50
|
}
|
|
@@ -65,23 +62,9 @@ function mapValue(
|
|
|
65
62
|
setAttribute(data, element, name, value);
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let property: string | undefined;
|
|
73
|
-
|
|
74
|
-
if (name === PROPERTY_CHECKED && element.type === INPUT_TYPE_CHECKBOX) {
|
|
75
|
-
property = PROPERTY_CHECKED;
|
|
76
|
-
} else if (name === PROPERTY_VALUE) {
|
|
77
|
-
property = PROPERTY_VALUE;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (property != null) {
|
|
81
|
-
mapEvent(element, EVENT_ON_PREFIXED, () => {
|
|
82
|
-
const next = element[property as keyof typeof element];
|
|
83
|
-
|
|
84
|
-
value.set(parse(String(next)) ?? next);
|
|
65
|
+
if (isInputElement(element) && isSignal(value) && name in element) {
|
|
66
|
+
mapEvent(element, EVENT_ON_VALUE, () => {
|
|
67
|
+
value.set(element[name as keyof typeof element]);
|
|
85
68
|
});
|
|
86
69
|
}
|
|
87
70
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {getString} from '@oscarpalmer/atoms/string';
|
|
3
|
+
import {camelCase} from '@oscarpalmer/atoms/string/case';
|
|
4
|
+
import {isReactive} from '@oscarpalmer/mora';
|
|
5
|
+
import {setAttribute as setTorettoAttribute} from '@oscarpalmer/toretto/attribute';
|
|
6
|
+
import {setStyle} from '@oscarpalmer/toretto/style';
|
|
7
|
+
import {
|
|
8
|
+
ATTRIBUTE_CLASS_PREFIX_LENGTH,
|
|
9
|
+
ATTRIBUTE_NAME_DELIMITER,
|
|
10
|
+
EXPRESSION_ATTRIBUTE_CLASS,
|
|
11
|
+
EXPRESSION_ATTRIBUTE_STYLE_FULL,
|
|
12
|
+
EXPRESSION_ATTRIBUTE_STYLE_PREFIX,
|
|
13
|
+
EXPRESSION_ATTRIBUTE_STYLE_PROPERTY,
|
|
14
|
+
PROPERTY_CHECKED,
|
|
15
|
+
PROPERTY_VALUE,
|
|
16
|
+
VALUE_TRUE,
|
|
17
|
+
} from '../constants';
|
|
18
|
+
import type {FragmentData} from '../models';
|
|
19
|
+
|
|
20
|
+
function getStyleValue(value: unknown, unit: string, isProperty: boolean): string | undefined {
|
|
21
|
+
if (removeStyleValue(value, unit, isProperty)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return value === true || value === VALUE_TRUE ? unit : `${getString(value)}${unit ?? ''}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removeStyleValue(value: unknown, unit: unknown, isProperty: boolean): boolean {
|
|
29
|
+
if (value == null || value === false || (isProperty && isNullableOrWhitespace(value))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return unit == null && (value === true || value === VALUE_TRUE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setAttribute(
|
|
37
|
+
data: FragmentData,
|
|
38
|
+
element: HTMLElement | SVGElement,
|
|
39
|
+
name: string,
|
|
40
|
+
value: unknown,
|
|
41
|
+
): void {
|
|
42
|
+
switch (true) {
|
|
43
|
+
case EXPRESSION_ATTRIBUTE_CLASS.test(name):
|
|
44
|
+
setClassValues(data, element, name, value);
|
|
45
|
+
return;
|
|
46
|
+
|
|
47
|
+
case EXPRESSION_ATTRIBUTE_STYLE_PREFIX.test(name):
|
|
48
|
+
setStyleValues(data, element, name, value);
|
|
49
|
+
return;
|
|
50
|
+
|
|
51
|
+
default:
|
|
52
|
+
setValue(data, element, name, value);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setClassValues(
|
|
58
|
+
data: FragmentData,
|
|
59
|
+
element: HTMLElement | SVGElement,
|
|
60
|
+
name: string,
|
|
61
|
+
value: unknown,
|
|
62
|
+
): void {
|
|
63
|
+
function update(value: unknown): void {
|
|
64
|
+
if (value === true || value === VALUE_TRUE) {
|
|
65
|
+
element.classList.add(...classes);
|
|
66
|
+
} else {
|
|
67
|
+
element.classList.remove(...classes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const classes = name.slice(ATTRIBUTE_CLASS_PREFIX_LENGTH).split(ATTRIBUTE_NAME_DELIMITER);
|
|
72
|
+
|
|
73
|
+
if (isReactive(value)) {
|
|
74
|
+
data.mora.subscribers.add(value.subscribe(update));
|
|
75
|
+
} else {
|
|
76
|
+
update(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setStyleValues(
|
|
81
|
+
data: FragmentData,
|
|
82
|
+
element: HTMLElement | SVGElement,
|
|
83
|
+
name: string,
|
|
84
|
+
value: unknown,
|
|
85
|
+
): void {
|
|
86
|
+
let [, property, unit] = EXPRESSION_ATTRIBUTE_STYLE_FULL.exec(name)!;
|
|
87
|
+
|
|
88
|
+
const isProperty = EXPRESSION_ATTRIBUTE_STYLE_PROPERTY.test(name);
|
|
89
|
+
|
|
90
|
+
property = camelCase(property);
|
|
91
|
+
|
|
92
|
+
if (isProperty) {
|
|
93
|
+
property = `--${property}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function update(value: unknown): void {
|
|
97
|
+
setStyle(
|
|
98
|
+
element,
|
|
99
|
+
property as keyof CSSStyleDeclaration,
|
|
100
|
+
getStyleValue(value, unit, isProperty),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isReactive(value)) {
|
|
105
|
+
data.mora.subscribers.add(value.subscribe(update));
|
|
106
|
+
} else {
|
|
107
|
+
update(value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function setValue(
|
|
112
|
+
data: FragmentData,
|
|
113
|
+
element: HTMLElement | SVGElement,
|
|
114
|
+
name: string,
|
|
115
|
+
value: unknown,
|
|
116
|
+
): void {
|
|
117
|
+
const isReactiveValue = isReactive(value);
|
|
118
|
+
|
|
119
|
+
unsetValue(element, name, isReactiveValue ? value.peek() : value);
|
|
120
|
+
|
|
121
|
+
if (isReactiveValue) {
|
|
122
|
+
data.mora.subscribers.add(
|
|
123
|
+
value.subscribe(next => {
|
|
124
|
+
setTorettoAttribute(element, name, next);
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
setTorettoAttribute(element, name, value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unsetValue(element: HTMLElement | SVGElement, name: string, value: unknown): void {
|
|
133
|
+
if (name === PROPERTY_CHECKED) {
|
|
134
|
+
(element as HTMLInputElement).checked = !(value === true || value === VALUE_TRUE);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (name === PROPERTY_VALUE) {
|
|
138
|
+
(element as HTMLInputElement).value = (element as HTMLInputElement).value === '' ? '_' : '';
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const ATTRIBUTE_CLASS_PREFIX_LENGTH = 6;
|
|
|
8
8
|
|
|
9
9
|
export const ATTRIBUTE_NAME_DELIMITER = '.';
|
|
10
10
|
|
|
11
|
+
export const CHANGE_INPUTS = new Set(['checkbox', 'radio']);
|
|
12
|
+
|
|
11
13
|
export const ERROR_FRAGMENT = 'Fragment function must return a Fragment instance';
|
|
12
14
|
|
|
13
15
|
export const ERROR_IDENTIFIER_DUPLICATE = "Duplicate identifier found: '<id>'";
|
|
@@ -29,17 +31,15 @@ export const EVENT_DEFAULTS: Record<string, string> = {
|
|
|
29
31
|
TEXTAREA: EVENT_INPUT,
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
export const EVENT_ON_PREFIXED = '@on';
|
|
33
|
-
|
|
34
34
|
export const EVENT_ON_VALUE = 'on';
|
|
35
35
|
|
|
36
36
|
export const EVENT_OPTIONS_DELIMITER = ':';
|
|
37
37
|
|
|
38
|
-
export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-]+)="<!--abydon.(\d+)-->"/g;
|
|
38
|
+
export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-.]+(?::[a-z:]+)?)="<!--abydon.(\d+)-->"/g;
|
|
39
39
|
|
|
40
|
-
export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^
|
|
40
|
+
export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^abydon-/;
|
|
41
41
|
|
|
42
|
-
export const EXPRESSION_ABYDON_CONTENT =
|
|
42
|
+
export const EXPRESSION_ABYDON_CONTENT = /^abydon\.(\d+)$/;
|
|
43
43
|
|
|
44
44
|
export const EXPRESSION_ATTRIBUTE_CLASS = /^class\./;
|
|
45
45
|
|
|
@@ -47,9 +47,11 @@ export const EXPRESSION_ATTRIBUTE_STYLE_FULL = /^style\.([\w-]+)(?:\.([\w-]+))?$
|
|
|
47
47
|
|
|
48
48
|
export const EXPRESSION_ATTRIBUTE_STYLE_PREFIX = /^style\./;
|
|
49
49
|
|
|
50
|
-
export const
|
|
50
|
+
export const EXPRESSION_ATTRIBUTE_STYLE_PROPERTY = /^style\.--/;
|
|
51
|
+
|
|
52
|
+
export const EXPRESSION_EVENT_ATTRIBUTE = /^@([\w-]+)(?::([a-z:]+))?="$/i;
|
|
51
53
|
|
|
52
|
-
export const EXPRESSION_EVENT_NAME =
|
|
54
|
+
export const EXPRESSION_EVENT_NAME = /^@?([\w-]+)(?::([a-z:]+))?$/i;
|
|
53
55
|
|
|
54
56
|
export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(ctive)$/i;
|
|
55
57
|
|
|
@@ -61,10 +63,12 @@ export const EXPRESSION_EVENT_PREFIX = /^@/;
|
|
|
61
63
|
|
|
62
64
|
export const EXPRESSION_PERIOD = /\./;
|
|
63
65
|
|
|
64
|
-
export const EXPRESSION_TEXTAREA_VALUE =
|
|
66
|
+
export const EXPRESSION_TEXTAREA_VALUE = /(?:<|<)!--abydon\.(\d+)--(?:>|>)/;
|
|
65
67
|
|
|
66
68
|
export const INPUT_TYPE_CHECKBOX = 'checkbox';
|
|
67
69
|
|
|
70
|
+
export const INPUT_TYPE_RADIO = 'radio';
|
|
71
|
+
|
|
68
72
|
export const NAME_FRAGMENT = '$fragment';
|
|
69
73
|
|
|
70
74
|
export const NAME_FRAGMENTS = '$fragments';
|
|
@@ -76,3 +80,7 @@ export const PROPERTY_IDENTIFIER = 'identifier';
|
|
|
76
80
|
export const PROPERTY_VALUE = 'value';
|
|
77
81
|
|
|
78
82
|
export const TEMPLATE_ITEM = '<>';
|
|
83
|
+
|
|
84
|
+
export const VALUE_TRUE = 'true';
|
|
85
|
+
|
|
86
|
+
export const WHITESPACE = /\s+/g;
|
package/src/fragment.ts
CHANGED
|
@@ -16,6 +16,13 @@ export class Fragment {
|
|
|
16
16
|
cache: true,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Is template caching enabled?
|
|
21
|
+
*/
|
|
22
|
+
get caching(): boolean {
|
|
23
|
+
return this.#configuration.cache;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
/**
|
|
20
27
|
* Fragment identifier
|
|
21
28
|
*/
|
|
@@ -91,14 +98,12 @@ export class Fragment {
|
|
|
91
98
|
|
|
92
99
|
mapNodes(
|
|
93
100
|
data,
|
|
94
|
-
data.items.flatMap(
|
|
95
|
-
item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
|
|
96
|
-
),
|
|
101
|
+
data.items.flatMap(item => item.nodes!),
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
return data.items.flatMap(
|
|
101
|
-
item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes
|
|
106
|
+
item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes!,
|
|
102
107
|
);
|
|
103
108
|
}
|
|
104
109
|
|
|
@@ -137,7 +142,7 @@ function removeFragment(data: FragmentData): void {
|
|
|
137
142
|
fragments?.[fragmentIndex]?.remove();
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
removeNodes(nodes
|
|
145
|
+
removeNodes(nodes!);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
data.items.length = 0;
|
package/src/fragments.ts
CHANGED
|
@@ -14,13 +14,6 @@ import type {FragmentsState} from './models';
|
|
|
14
14
|
export class Fragments {
|
|
15
15
|
readonly #state: FragmentsState;
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Fragment items
|
|
19
|
-
*/
|
|
20
|
-
get items(): ReactiveArray<Fragment> {
|
|
21
|
-
return this.#state.mapped;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
constructor(
|
|
25
18
|
items: ReactiveArray<unknown>,
|
|
26
19
|
identify: (item: unknown) => unknown,
|
|
@@ -39,18 +32,16 @@ export class Fragments {
|
|
|
39
32
|
subscriber: undefined,
|
|
40
33
|
};
|
|
41
34
|
|
|
42
|
-
|
|
35
|
+
fragmentsStates.set(this, this.#state);
|
|
43
36
|
|
|
44
37
|
initializeFragments(this.#state);
|
|
45
38
|
}
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
export function handleFragments(item: Fragments | FragmentsState, remove: boolean): void {
|
|
49
|
-
const state = isFragments(item) ?
|
|
42
|
+
const state = isFragments(item) ? fragmentsStates.get(item) : item;
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
(remove ? removeFragments : initializeFragments)(state);
|
|
53
|
-
}
|
|
44
|
+
(remove ? removeFragments : initializeFragments)(state!);
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
function handleItems(state: FragmentsState, items: unknown[]): void {
|
|
@@ -135,4 +126,4 @@ function updateFragments(state: FragmentsState, active?: Set<string>): void {
|
|
|
135
126
|
|
|
136
127
|
//
|
|
137
128
|
|
|
138
|
-
const
|
|
129
|
+
export const fragmentsStates: WeakMap<Fragments, FragmentsState> = new WeakMap();
|
package/src/helpers/index.ts
CHANGED
|
@@ -27,10 +27,20 @@ export function compareArrays(
|
|
|
27
27
|
return firstIsLarger ? ARRAY_COMPARISON_REMOVED : ARRAY_COMPARISON_ADDED;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Is the value a Fragment?
|
|
32
|
+
* @param value Value to check
|
|
33
|
+
* @returns `true` if the value is a Fragment, otherwise `false`
|
|
34
|
+
*/
|
|
30
35
|
export function isFragment(value: unknown): value is Fragment {
|
|
31
36
|
return isNamed(value, NAME_FRAGMENT);
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Is the value a Fragments?
|
|
41
|
+
* @param value Value to check
|
|
42
|
+
* @returns `true` if the value is a Fragments, otherwise `false`
|
|
43
|
+
*/
|
|
34
44
|
export function isFragments(value: unknown): value is Fragments {
|
|
35
45
|
return isNamed(value, NAME_FRAGMENTS);
|
|
36
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import '@oscarpalmer/mora';
|
|
2
|
-
import type
|
|
2
|
+
import {isArray, type ReactiveArray} from '@oscarpalmer/mora';
|
|
3
3
|
import {Fragment} from './fragment';
|
|
4
4
|
import {Fragments} from './fragments';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Create Fragments from a reactive array
|
|
7
|
+
* Create a Fragments from a reactive array
|
|
8
8
|
* @param array Reactive array
|
|
9
9
|
* @param identify Function to identify item
|
|
10
10
|
* @param fragment Function to create fragment from item
|
|
@@ -15,11 +15,23 @@ export function fragments<Item>(
|
|
|
15
15
|
identify: (item: Item) => unknown,
|
|
16
16
|
fragment: (item: Item) => Fragment,
|
|
17
17
|
): Fragments {
|
|
18
|
+
if (!isArray(array)) {
|
|
19
|
+
throw new TypeError('Fragments array must be a reactive array');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof identify !== 'function') {
|
|
23
|
+
throw new TypeError('Fragments identify must be a function');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof fragment !== 'function') {
|
|
27
|
+
throw new TypeError('Fragments fragment must be a function');
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
return new Fragments(array as ReactiveArray<unknown>, identify as never, fragment as never);
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
/**
|
|
22
|
-
* Create Fragment from a template
|
|
34
|
+
* Create a Fragment from a template
|
|
23
35
|
* @returns Fragment
|
|
24
36
|
*/
|
|
25
37
|
export function html(template: TemplateStringsArray, ...values: unknown[]): Fragment {
|
|
@@ -29,3 +41,4 @@ export function html(template: TemplateStringsArray, ...values: unknown[]): Frag
|
|
|
29
41
|
export * from '@oscarpalmer/mora';
|
|
30
42
|
export type {Fragment} from './fragment';
|
|
31
43
|
export type {Fragments} from './fragments';
|
|
44
|
+
export {isFragment, isFragments} from './helpers';
|
package/src/node/event.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {on} from '@oscarpalmer/toretto/event';
|
|
2
2
|
import {
|
|
3
|
+
CHANGE_INPUTS,
|
|
3
4
|
EVENT_CHANGE,
|
|
4
5
|
EVENT_DEFAULTS,
|
|
5
6
|
EVENT_INPUT,
|
|
6
7
|
EVENT_ON_VALUE,
|
|
7
8
|
EVENT_OPTIONS_DELIMITER,
|
|
8
9
|
EVENT_SUBMIT,
|
|
9
|
-
EXPRESSION_EVENT_CHANGE_TYPES,
|
|
10
10
|
EXPRESSION_EVENT_NAME,
|
|
11
11
|
EXPRESSION_EVENT_OPTIONS_ACTIVE,
|
|
12
12
|
EXPRESSION_EVENT_OPTIONS_CAPTURE,
|
|
@@ -29,7 +29,7 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (element instanceof HTMLInputElement) {
|
|
32
|
-
if (
|
|
32
|
+
if (CHANGE_INPUTS.has(element.type)) {
|
|
33
33
|
return EVENT_CHANGE;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -40,9 +40,16 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function mapEvent(element: HTMLElement | SVGElement, name: string, value: unknown): void {
|
|
43
|
-
const [, type, options] = EXPRESSION_EVENT_NAME.exec(name)
|
|
43
|
+
const [, type, options] = EXPRESSION_EVENT_NAME.exec(name)!;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const values = Array.isArray(value) ? value : [value];
|
|
46
|
+
const {length} = values;
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < length; index += 1) {
|
|
49
|
+
const item = values[index];
|
|
50
|
+
|
|
51
|
+
if (typeof item === 'function') {
|
|
52
|
+
on(element, getType(element, type), item as EventListener, getOptions(options ?? ''));
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
55
|
}
|
package/src/node/index.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {getString} from '@oscarpalmer/atoms/string';
|
|
2
3
|
import {
|
|
3
4
|
computed,
|
|
4
5
|
isComputed,
|
|
5
6
|
isReactive,
|
|
6
7
|
isSignal,
|
|
7
8
|
type Computed,
|
|
8
|
-
type
|
|
9
|
+
type ReactiveArray,
|
|
9
10
|
} from '@oscarpalmer/mora';
|
|
10
11
|
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
12
|
+
import {mapAttributes} from '../attribute/index';
|
|
13
|
+
import {EVENT_ON_VALUE, EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
|
|
14
|
+
import {Fragments, fragmentsStates, handleFragments} from '../fragments';
|
|
13
15
|
import {isFragment, isFragments} from '../helpers';
|
|
14
16
|
import {createNodes} from '../helpers/dom';
|
|
15
17
|
import type {FragmentData} from '../models';
|
|
16
|
-
import {mapAttributes} from './attribute';
|
|
17
|
-
import {setReactiveValue} from './value';
|
|
18
18
|
import {mapEvent} from './event';
|
|
19
|
+
import {setReactiveValue} from './value';
|
|
19
20
|
|
|
20
21
|
function mapNode(data: FragmentData, comment: Comment): void {
|
|
21
|
-
const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent
|
|
22
|
+
const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent);
|
|
22
23
|
const value = matches == null ? null : data.values[+matches[1]];
|
|
23
24
|
|
|
24
25
|
if (value != null) {
|
|
@@ -53,23 +54,33 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
|
|
56
|
-
const [, index] =
|
|
57
|
+
const [, index] =
|
|
58
|
+
EXPRESSION_TEXTAREA_VALUE.exec(element.textContent) ??
|
|
59
|
+
EXPRESSION_TEXTAREA_VALUE.exec(element.value) ??
|
|
60
|
+
[];
|
|
57
61
|
|
|
58
62
|
if (index == null) {
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
element.textContent = '';
|
|
62
67
|
element.value = '';
|
|
63
68
|
|
|
64
69
|
const value = data.values[Number.parseInt(index, 10)];
|
|
65
70
|
|
|
66
71
|
if (isSignal(value)) {
|
|
67
|
-
element.value =
|
|
72
|
+
element.value = getString(value.peek());
|
|
68
73
|
|
|
69
|
-
mapEvent(element,
|
|
74
|
+
mapEvent(element, EVENT_ON_VALUE, () => {
|
|
70
75
|
value.set(element.value);
|
|
71
76
|
});
|
|
72
77
|
|
|
78
|
+
data.mora.subscribers.add(
|
|
79
|
+
value.subscribe(value => {
|
|
80
|
+
element.value = getString(value);
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
73
84
|
return;
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -82,11 +93,11 @@ function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
|
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
if (reactive == null) {
|
|
85
|
-
element.value =
|
|
96
|
+
element.value = '';
|
|
86
97
|
} else {
|
|
87
98
|
data.mora.subscribers.add(
|
|
88
99
|
reactive.subscribe(value => {
|
|
89
|
-
element.value =
|
|
100
|
+
element.value = getString(value);
|
|
90
101
|
}),
|
|
91
102
|
);
|
|
92
103
|
}
|
|
@@ -99,8 +110,7 @@ function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
|
|
|
99
110
|
break;
|
|
100
111
|
|
|
101
112
|
case isFragments(value):
|
|
102
|
-
|
|
103
|
-
setReactiveValue(data, comment, value.items as Reactive<unknown[], unknown>);
|
|
113
|
+
setFragmentsValue(data, comment, value);
|
|
104
114
|
break;
|
|
105
115
|
|
|
106
116
|
case isReactive(value):
|
|
@@ -132,3 +142,11 @@ function setComputedValue(data: FragmentData, comment: Comment, callback: Generi
|
|
|
132
142
|
|
|
133
143
|
setReactiveValue(data, comment, value);
|
|
134
144
|
}
|
|
145
|
+
|
|
146
|
+
function setFragmentsValue(data: FragmentData, comment: Comment, fragments: Fragments): void {
|
|
147
|
+
const state = fragmentsStates.get(fragments)!;
|
|
148
|
+
|
|
149
|
+
handleFragments(state, false);
|
|
150
|
+
|
|
151
|
+
setReactiveValue(data, comment, state.mapped as ReactiveArray<unknown>);
|
|
152
|
+
}
|
package/src/node/value.ts
CHANGED
|
@@ -113,11 +113,7 @@ function replaceText(item: FragmentItem, comment: Comment, isNullable: boolean):
|
|
|
113
113
|
replaceNodes(item.nodes ?? [], to);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function setArray(
|
|
117
|
-
item: FragmentItem,
|
|
118
|
-
comment: Comment,
|
|
119
|
-
value: unknown[],
|
|
120
|
-
): Partial<FragmentItem> | boolean {
|
|
116
|
+
function setArray(item: FragmentItem, comment: Comment, value: unknown[]): Partial<FragmentItem> {
|
|
121
117
|
if (value.length === 0) {
|
|
122
118
|
return {
|
|
123
119
|
nodes: setText(item, comment, value),
|
|
@@ -129,7 +125,9 @@ function setArray(
|
|
|
129
125
|
const next = templates.map(fragment => fragment.identifier) as unknown[];
|
|
130
126
|
const previous = item.fragments?.map(fragment => fragment.identifier) ?? [];
|
|
131
127
|
|
|
132
|
-
|
|
128
|
+
const nextSet = new Set(next);
|
|
129
|
+
|
|
130
|
+
if (nextSet.size !== templates.length) {
|
|
133
131
|
templates = [];
|
|
134
132
|
}
|
|
135
133
|
|
|
@@ -150,7 +148,7 @@ function setArray(
|
|
|
150
148
|
|
|
151
149
|
return handleArray(
|
|
152
150
|
{
|
|
153
|
-
next:
|
|
151
|
+
next: nextSet,
|
|
154
152
|
previous: new Set(previous),
|
|
155
153
|
},
|
|
156
154
|
{
|
|
@@ -163,11 +161,7 @@ function setArray(
|
|
|
163
161
|
|
|
164
162
|
function setNodes(item: FragmentItem, comment: Comment, next: ChildNode[]): ChildNode[] {
|
|
165
163
|
if (item.nodes == null) {
|
|
166
|
-
|
|
167
|
-
replaceNodes([comment], next);
|
|
168
|
-
} else if (item.text?.parentNode != null) {
|
|
169
|
-
replaceNodes([item.text], next);
|
|
170
|
-
}
|
|
164
|
+
replaceNodes([comment], next);
|
|
171
165
|
} else {
|
|
172
166
|
replaceNodes(item.nodes, next);
|
|
173
167
|
}
|
|
@@ -196,25 +190,16 @@ export function setReactiveValue(
|
|
|
196
190
|
setReactiveValueForSingle(item, comment, value);
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
item.nodes
|
|
193
|
+
item.nodes ??= [comment];
|
|
200
194
|
}),
|
|
201
195
|
);
|
|
202
196
|
}
|
|
203
197
|
|
|
204
198
|
function setReactiveValueForArray(item: FragmentItem, comment: Comment, value: unknown[]): void {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
item.fragments = typeof result === 'boolean' ? undefined : result?.fragments;
|
|
199
|
+
const {fragments, nodes} = setArray(item, comment, value);
|
|
208
200
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
item.nodes = item.text == null ? [] : [item.text];
|
|
212
|
-
} else {
|
|
213
|
-
item.nodes = undefined;
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
item.nodes = result?.nodes;
|
|
217
|
-
}
|
|
201
|
+
item.fragments = fragments;
|
|
202
|
+
item.nodes = nodes;
|
|
218
203
|
}
|
|
219
204
|
|
|
220
205
|
function setReactiveValueForSingle(item: FragmentItem, comment: Comment, value: unknown): void {
|