@thomas-siegfried/tapout 0.0.1 → 0.0.2
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/LICENSE +21 -21
- package/README.md +1205 -1205
- package/package.json +55 -55
- package/src/applyBindings.ts +372 -372
- package/src/arrayToDomMapping.ts +300 -300
- package/src/attributeInterpolationMarkup.ts +91 -91
- package/src/bindingContext.ts +207 -207
- package/src/bindingEvent.ts +198 -198
- package/src/bindingProvider.ts +260 -260
- package/src/bindings.ts +963 -963
- package/src/compareArrays.ts +122 -122
- package/src/componentBinding.ts +318 -318
- package/src/componentDecorator.ts +62 -62
- package/src/components.ts +367 -367
- package/src/computed.ts +439 -439
- package/src/configure.ts +52 -52
- package/src/controlFlowBindings.ts +206 -206
- package/src/core.ts +60 -60
- package/src/decorators.ts +407 -407
- package/src/dependencyDetection.ts +76 -76
- package/src/disposable.ts +36 -36
- package/src/domData.ts +42 -42
- package/src/domNodeDisposal.ts +94 -94
- package/src/effects.ts +29 -29
- package/src/event.ts +173 -173
- package/src/expressionRewriting.ts +219 -219
- package/src/extenders.ts +102 -102
- package/src/filters.ts +91 -91
- package/src/index.ts +150 -150
- package/src/interpolationMarkup.ts +130 -130
- package/src/memoization.ts +71 -71
- package/src/namespacedBindings.ts +132 -132
- package/src/observable.ts +48 -48
- package/src/observableArray.ts +397 -397
- package/src/options.ts +25 -25
- package/src/selectExtensions.ts +68 -68
- package/src/slotBinding.ts +84 -84
- package/src/subscribable.ts +266 -266
- package/src/tasks.ts +79 -79
- package/src/templateEngine.ts +108 -108
- package/src/templateRendering.ts +399 -399
- package/src/templateRewriting.ts +94 -94
- package/src/templateSources.ts +134 -134
- package/src/utils.ts +123 -123
- package/src/utilsDom.ts +87 -87
- package/src/virtualElements.ts +153 -153
- package/src/wireParams.ts +49 -49
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
import { Observable } from './observable.js';
|
|
2
|
-
import { Computed } from './computed.js';
|
|
3
|
-
import { isSubscribable } from './subscribable.js';
|
|
4
|
-
|
|
5
|
-
// ---- Types ----
|
|
6
|
-
|
|
7
|
-
export interface KeyValuePair {
|
|
8
|
-
key?: string;
|
|
9
|
-
value?: string;
|
|
10
|
-
unknown?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface PreProcessOptions {
|
|
14
|
-
valueAccessors?: boolean;
|
|
15
|
-
bindingParams?: boolean;
|
|
16
|
-
getBindingHandler?: (key: string) => { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface AllBindingsAccessor {
|
|
20
|
-
get(key: string): unknown;
|
|
21
|
-
has(key: string): boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---- Two-way binding registry ----
|
|
25
|
-
|
|
26
|
-
export const twoWayBindings: Record<string, boolean | string> = {};
|
|
27
|
-
|
|
28
|
-
// ---- Token regex ----
|
|
29
|
-
|
|
30
|
-
const javaScriptReservedWords = ['true', 'false', 'null', 'undefined'];
|
|
31
|
-
|
|
32
|
-
// Matches something assignable: an isolated identifier or something ending with a property accessor.
|
|
33
|
-
const javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
|
|
34
|
-
|
|
35
|
-
const specials = ',"\'`{}()/:[\\]';
|
|
36
|
-
const bindingToken = RegExp([
|
|
37
|
-
'"(?:\\\\.|[^"])*"', // double-quoted string
|
|
38
|
-
"'(?:\\\\.|[^'])*'", // single-quoted string
|
|
39
|
-
'`(?:\\\\.|[^`])*`', // template literal
|
|
40
|
-
'/\\*(?:[^*]|\\*+[^*/])*\\*+/', // block comment
|
|
41
|
-
'//.*\n', // line comment
|
|
42
|
-
'/(?:\\\\.|[^/])+/\\w*', // regex literal
|
|
43
|
-
'[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', // multi-char token
|
|
44
|
-
'[^\\s]', // single char
|
|
45
|
-
].join('|'), 'g');
|
|
46
|
-
|
|
47
|
-
const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/;
|
|
48
|
-
const keywordRegexLookBehind: Record<string, number> = { 'in': 1, 'return': 1, 'typeof': 1 };
|
|
49
|
-
|
|
50
|
-
// ---- Core functions ----
|
|
51
|
-
|
|
52
|
-
function getWriteableValue(expression: string): string | false {
|
|
53
|
-
if (javaScriptReservedWords.indexOf(expression) >= 0) return false;
|
|
54
|
-
const match = expression.match(javaScriptAssignmentTarget);
|
|
55
|
-
if (match === null) return false;
|
|
56
|
-
return match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function parseObjectLiteral(objectLiteralString: string): KeyValuePair[] {
|
|
60
|
-
let str = objectLiteralString.trim();
|
|
61
|
-
|
|
62
|
-
// Strip surrounding braces
|
|
63
|
-
if (str.charCodeAt(0) === 123 /* { */) str = str.slice(1, -1);
|
|
64
|
-
|
|
65
|
-
// Append newline + comma so the last pair terminates uniformly
|
|
66
|
-
str += '\n,';
|
|
67
|
-
|
|
68
|
-
const toks = str.match(bindingToken);
|
|
69
|
-
const result: KeyValuePair[] = [];
|
|
70
|
-
|
|
71
|
-
if (!toks || toks.length <= 1) return result;
|
|
72
|
-
|
|
73
|
-
let key: string | undefined;
|
|
74
|
-
let values: string[] = [];
|
|
75
|
-
let depth = 0;
|
|
76
|
-
|
|
77
|
-
for (let i = 0, tok: string; (tok = toks[i]); ++i) {
|
|
78
|
-
const c = tok.charCodeAt(0);
|
|
79
|
-
|
|
80
|
-
if (c === 44 /* , */) {
|
|
81
|
-
if (depth <= 0) {
|
|
82
|
-
result.push(
|
|
83
|
-
(key && values.length)
|
|
84
|
-
? { key, value: values.join('') }
|
|
85
|
-
: { unknown: key || values.join('') },
|
|
86
|
-
);
|
|
87
|
-
key = undefined;
|
|
88
|
-
depth = 0;
|
|
89
|
-
values = [];
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
} else if (c === 58 /* : */) {
|
|
93
|
-
if (!depth && !key && values.length === 1) {
|
|
94
|
-
key = values.pop()!;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
} else if (c === 47 /* / */ && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {
|
|
98
|
-
// Line or block comment — skip
|
|
99
|
-
continue;
|
|
100
|
-
} else if (c === 47 /* / */ && i && tok.length > 1) {
|
|
101
|
-
// Disambiguate regex vs division
|
|
102
|
-
const prevMatch = toks[i - 1].match(divisionLookBehind);
|
|
103
|
-
if (prevMatch && !keywordRegexLookBehind[prevMatch[0]]) {
|
|
104
|
-
str = str.substr(str.indexOf(tok) + 1);
|
|
105
|
-
const newToks = str.match(bindingToken);
|
|
106
|
-
if (newToks) {
|
|
107
|
-
toks.length = 0;
|
|
108
|
-
toks.push(...newToks);
|
|
109
|
-
}
|
|
110
|
-
i = -1;
|
|
111
|
-
tok = '/';
|
|
112
|
-
}
|
|
113
|
-
} else if (c === 40 || c === 123 || c === 91) {
|
|
114
|
-
// ( { [
|
|
115
|
-
++depth;
|
|
116
|
-
} else if (c === 41 || c === 125 || c === 93) {
|
|
117
|
-
// ) } ]
|
|
118
|
-
--depth;
|
|
119
|
-
} else if (!key && !values.length && (c === 34 || c === 39)) {
|
|
120
|
-
// Quoted key — strip quotes
|
|
121
|
-
tok = tok.slice(1, -1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
values.push(tok);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (depth > 0) {
|
|
128
|
-
throw new Error('Unbalanced parentheses, braces, or brackets');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function preProcessBindings(
|
|
135
|
-
bindingsStringOrKeyValueArray: string | KeyValuePair[],
|
|
136
|
-
options?: PreProcessOptions,
|
|
137
|
-
): string {
|
|
138
|
-
options = options || {};
|
|
139
|
-
|
|
140
|
-
const resultStrings: string[] = [];
|
|
141
|
-
const propertyAccessorResultStrings: string[] = [];
|
|
142
|
-
const makeValueAccessors = options.valueAccessors;
|
|
143
|
-
const bindingParams = options.bindingParams;
|
|
144
|
-
const getBindingHandler = options.getBindingHandler;
|
|
145
|
-
|
|
146
|
-
const keyValueArray = typeof bindingsStringOrKeyValueArray === 'string'
|
|
147
|
-
? parseObjectLiteral(bindingsStringOrKeyValueArray)
|
|
148
|
-
: bindingsStringOrKeyValueArray;
|
|
149
|
-
|
|
150
|
-
function processKeyValue(key: string, val: string) {
|
|
151
|
-
let writableVal: string | false;
|
|
152
|
-
|
|
153
|
-
function callPreprocessHook(
|
|
154
|
-
obj: { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined,
|
|
155
|
-
): boolean {
|
|
156
|
-
if (obj && obj.preprocess) {
|
|
157
|
-
const result = obj.preprocess(val, key, processKeyValue);
|
|
158
|
-
if (result !== undefined) val = result;
|
|
159
|
-
return result !== undefined;
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!bindingParams) {
|
|
165
|
-
if (getBindingHandler && !callPreprocessHook(getBindingHandler(key))) return;
|
|
166
|
-
|
|
167
|
-
if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
|
|
168
|
-
const writeKey = typeof twoWayBindings[key] === 'string' ? twoWayBindings[key] as string : key;
|
|
169
|
-
propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + '=_z}');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (makeValueAccessors) {
|
|
174
|
-
val = 'function(){return ' + val + ' }';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
resultStrings.push("'" + key + "':" + val);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
for (const kv of keyValueArray) {
|
|
181
|
-
processKeyValue(kv.key || kv.unknown || '', kv.value || '');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (propertyAccessorResultStrings.length) {
|
|
185
|
-
processKeyValue('_tap_property_writers', '{' + propertyAccessorResultStrings.join(',') + ' }');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return resultStrings.join(',');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function keyValueArrayContainsKey(keyValueArray: KeyValuePair[], key: string): boolean {
|
|
192
|
-
for (const kv of keyValueArray) {
|
|
193
|
-
if (kv.key === key) return true;
|
|
194
|
-
}
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function writeValueToProperty(
|
|
199
|
-
property: unknown,
|
|
200
|
-
allBindings: AllBindingsAccessor,
|
|
201
|
-
key: string,
|
|
202
|
-
value: unknown,
|
|
203
|
-
checkIfDifferent?: boolean,
|
|
204
|
-
): void {
|
|
205
|
-
if (!property || !isSubscribable(property)) {
|
|
206
|
-
const propWriters = allBindings.get('_tap_property_writers') as Record<string, (v: unknown) => void> | undefined;
|
|
207
|
-
if (propWriters && propWriters[key]) {
|
|
208
|
-
propWriters[key](value);
|
|
209
|
-
}
|
|
210
|
-
} else if (isWritable(property) && (!checkIfDifferent || property.peek() !== value)) {
|
|
211
|
-
property.set(value as never);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function isWritable(value: unknown): value is Observable<unknown> | Computed<unknown> {
|
|
216
|
-
if (value instanceof Observable) return true;
|
|
217
|
-
if (value instanceof Computed) return value.hasWriteFunction;
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
1
|
+
import { Observable } from './observable.js';
|
|
2
|
+
import { Computed } from './computed.js';
|
|
3
|
+
import { isSubscribable } from './subscribable.js';
|
|
4
|
+
|
|
5
|
+
// ---- Types ----
|
|
6
|
+
|
|
7
|
+
export interface KeyValuePair {
|
|
8
|
+
key?: string;
|
|
9
|
+
value?: string;
|
|
10
|
+
unknown?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreProcessOptions {
|
|
14
|
+
valueAccessors?: boolean;
|
|
15
|
+
bindingParams?: boolean;
|
|
16
|
+
getBindingHandler?: (key: string) => { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AllBindingsAccessor {
|
|
20
|
+
get(key: string): unknown;
|
|
21
|
+
has(key: string): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---- Two-way binding registry ----
|
|
25
|
+
|
|
26
|
+
export const twoWayBindings: Record<string, boolean | string> = {};
|
|
27
|
+
|
|
28
|
+
// ---- Token regex ----
|
|
29
|
+
|
|
30
|
+
const javaScriptReservedWords = ['true', 'false', 'null', 'undefined'];
|
|
31
|
+
|
|
32
|
+
// Matches something assignable: an isolated identifier or something ending with a property accessor.
|
|
33
|
+
const javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
|
|
34
|
+
|
|
35
|
+
const specials = ',"\'`{}()/:[\\]';
|
|
36
|
+
const bindingToken = RegExp([
|
|
37
|
+
'"(?:\\\\.|[^"])*"', // double-quoted string
|
|
38
|
+
"'(?:\\\\.|[^'])*'", // single-quoted string
|
|
39
|
+
'`(?:\\\\.|[^`])*`', // template literal
|
|
40
|
+
'/\\*(?:[^*]|\\*+[^*/])*\\*+/', // block comment
|
|
41
|
+
'//.*\n', // line comment
|
|
42
|
+
'/(?:\\\\.|[^/])+/\\w*', // regex literal
|
|
43
|
+
'[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', // multi-char token
|
|
44
|
+
'[^\\s]', // single char
|
|
45
|
+
].join('|'), 'g');
|
|
46
|
+
|
|
47
|
+
const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/;
|
|
48
|
+
const keywordRegexLookBehind: Record<string, number> = { 'in': 1, 'return': 1, 'typeof': 1 };
|
|
49
|
+
|
|
50
|
+
// ---- Core functions ----
|
|
51
|
+
|
|
52
|
+
function getWriteableValue(expression: string): string | false {
|
|
53
|
+
if (javaScriptReservedWords.indexOf(expression) >= 0) return false;
|
|
54
|
+
const match = expression.match(javaScriptAssignmentTarget);
|
|
55
|
+
if (match === null) return false;
|
|
56
|
+
return match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseObjectLiteral(objectLiteralString: string): KeyValuePair[] {
|
|
60
|
+
let str = objectLiteralString.trim();
|
|
61
|
+
|
|
62
|
+
// Strip surrounding braces
|
|
63
|
+
if (str.charCodeAt(0) === 123 /* { */) str = str.slice(1, -1);
|
|
64
|
+
|
|
65
|
+
// Append newline + comma so the last pair terminates uniformly
|
|
66
|
+
str += '\n,';
|
|
67
|
+
|
|
68
|
+
const toks = str.match(bindingToken);
|
|
69
|
+
const result: KeyValuePair[] = [];
|
|
70
|
+
|
|
71
|
+
if (!toks || toks.length <= 1) return result;
|
|
72
|
+
|
|
73
|
+
let key: string | undefined;
|
|
74
|
+
let values: string[] = [];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
|
|
77
|
+
for (let i = 0, tok: string; (tok = toks[i]); ++i) {
|
|
78
|
+
const c = tok.charCodeAt(0);
|
|
79
|
+
|
|
80
|
+
if (c === 44 /* , */) {
|
|
81
|
+
if (depth <= 0) {
|
|
82
|
+
result.push(
|
|
83
|
+
(key && values.length)
|
|
84
|
+
? { key, value: values.join('') }
|
|
85
|
+
: { unknown: key || values.join('') },
|
|
86
|
+
);
|
|
87
|
+
key = undefined;
|
|
88
|
+
depth = 0;
|
|
89
|
+
values = [];
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
} else if (c === 58 /* : */) {
|
|
93
|
+
if (!depth && !key && values.length === 1) {
|
|
94
|
+
key = values.pop()!;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
} else if (c === 47 /* / */ && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {
|
|
98
|
+
// Line or block comment — skip
|
|
99
|
+
continue;
|
|
100
|
+
} else if (c === 47 /* / */ && i && tok.length > 1) {
|
|
101
|
+
// Disambiguate regex vs division
|
|
102
|
+
const prevMatch = toks[i - 1].match(divisionLookBehind);
|
|
103
|
+
if (prevMatch && !keywordRegexLookBehind[prevMatch[0]]) {
|
|
104
|
+
str = str.substr(str.indexOf(tok) + 1);
|
|
105
|
+
const newToks = str.match(bindingToken);
|
|
106
|
+
if (newToks) {
|
|
107
|
+
toks.length = 0;
|
|
108
|
+
toks.push(...newToks);
|
|
109
|
+
}
|
|
110
|
+
i = -1;
|
|
111
|
+
tok = '/';
|
|
112
|
+
}
|
|
113
|
+
} else if (c === 40 || c === 123 || c === 91) {
|
|
114
|
+
// ( { [
|
|
115
|
+
++depth;
|
|
116
|
+
} else if (c === 41 || c === 125 || c === 93) {
|
|
117
|
+
// ) } ]
|
|
118
|
+
--depth;
|
|
119
|
+
} else if (!key && !values.length && (c === 34 || c === 39)) {
|
|
120
|
+
// Quoted key — strip quotes
|
|
121
|
+
tok = tok.slice(1, -1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
values.push(tok);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (depth > 0) {
|
|
128
|
+
throw new Error('Unbalanced parentheses, braces, or brackets');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function preProcessBindings(
|
|
135
|
+
bindingsStringOrKeyValueArray: string | KeyValuePair[],
|
|
136
|
+
options?: PreProcessOptions,
|
|
137
|
+
): string {
|
|
138
|
+
options = options || {};
|
|
139
|
+
|
|
140
|
+
const resultStrings: string[] = [];
|
|
141
|
+
const propertyAccessorResultStrings: string[] = [];
|
|
142
|
+
const makeValueAccessors = options.valueAccessors;
|
|
143
|
+
const bindingParams = options.bindingParams;
|
|
144
|
+
const getBindingHandler = options.getBindingHandler;
|
|
145
|
+
|
|
146
|
+
const keyValueArray = typeof bindingsStringOrKeyValueArray === 'string'
|
|
147
|
+
? parseObjectLiteral(bindingsStringOrKeyValueArray)
|
|
148
|
+
: bindingsStringOrKeyValueArray;
|
|
149
|
+
|
|
150
|
+
function processKeyValue(key: string, val: string) {
|
|
151
|
+
let writableVal: string | false;
|
|
152
|
+
|
|
153
|
+
function callPreprocessHook(
|
|
154
|
+
obj: { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined,
|
|
155
|
+
): boolean {
|
|
156
|
+
if (obj && obj.preprocess) {
|
|
157
|
+
const result = obj.preprocess(val, key, processKeyValue);
|
|
158
|
+
if (result !== undefined) val = result;
|
|
159
|
+
return result !== undefined;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!bindingParams) {
|
|
165
|
+
if (getBindingHandler && !callPreprocessHook(getBindingHandler(key))) return;
|
|
166
|
+
|
|
167
|
+
if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
|
|
168
|
+
const writeKey = typeof twoWayBindings[key] === 'string' ? twoWayBindings[key] as string : key;
|
|
169
|
+
propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + '=_z}');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (makeValueAccessors) {
|
|
174
|
+
val = 'function(){return ' + val + ' }';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resultStrings.push("'" + key + "':" + val);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const kv of keyValueArray) {
|
|
181
|
+
processKeyValue(kv.key || kv.unknown || '', kv.value || '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (propertyAccessorResultStrings.length) {
|
|
185
|
+
processKeyValue('_tap_property_writers', '{' + propertyAccessorResultStrings.join(',') + ' }');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return resultStrings.join(',');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function keyValueArrayContainsKey(keyValueArray: KeyValuePair[], key: string): boolean {
|
|
192
|
+
for (const kv of keyValueArray) {
|
|
193
|
+
if (kv.key === key) return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function writeValueToProperty(
|
|
199
|
+
property: unknown,
|
|
200
|
+
allBindings: AllBindingsAccessor,
|
|
201
|
+
key: string,
|
|
202
|
+
value: unknown,
|
|
203
|
+
checkIfDifferent?: boolean,
|
|
204
|
+
): void {
|
|
205
|
+
if (!property || !isSubscribable(property)) {
|
|
206
|
+
const propWriters = allBindings.get('_tap_property_writers') as Record<string, (v: unknown) => void> | undefined;
|
|
207
|
+
if (propWriters && propWriters[key]) {
|
|
208
|
+
propWriters[key](value);
|
|
209
|
+
}
|
|
210
|
+
} else if (isWritable(property) && (!checkIfDifferent || property.peek() !== value)) {
|
|
211
|
+
property.set(value as never);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isWritable(value: unknown): value is Observable<unknown> | Computed<unknown> {
|
|
216
|
+
if (value instanceof Observable) return true;
|
|
217
|
+
if (value instanceof Computed) return value.hasWriteFunction;
|
|
218
|
+
return false;
|
|
219
|
+
}
|
package/src/extenders.ts
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import type { Subscribable } from './subscribable.js';
|
|
2
|
-
import { valuesArePrimitiveAndEqual } from './subscribable.js';
|
|
3
|
-
import { schedule, cancel } from './tasks.js';
|
|
4
|
-
|
|
5
|
-
export type ExtenderHandler = (target: Subscribable, value: unknown) => Subscribable | void;
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
8
|
-
export interface ExtenderMap {
|
|
9
|
-
notify: 'always';
|
|
10
|
-
rateLimit: number | RateLimitOptions;
|
|
11
|
-
deferred: true;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type ExtenderOptions = Partial<ExtenderMap> & Record<string, unknown>;
|
|
15
|
-
|
|
16
|
-
const registry: Record<string, ExtenderHandler> = {};
|
|
17
|
-
|
|
18
|
-
export function registerExtender(name: string, handler: ExtenderHandler): void {
|
|
19
|
-
registry[name] = handler;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getExtenderHandler(name: string): ExtenderHandler | undefined {
|
|
23
|
-
return registry[name];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function throttle(callback: () => void, timeout: number): () => void {
|
|
27
|
-
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
28
|
-
return function () {
|
|
29
|
-
if (!timeoutInstance) {
|
|
30
|
-
timeoutInstance = setTimeout(function () {
|
|
31
|
-
timeoutInstance = undefined;
|
|
32
|
-
callback();
|
|
33
|
-
}, timeout);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function debounce(callback: () => void, timeout: number): () => void {
|
|
39
|
-
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
40
|
-
return function () {
|
|
41
|
-
clearTimeout(timeoutInstance);
|
|
42
|
-
timeoutInstance = setTimeout(callback, timeout);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface RateLimitOptions {
|
|
47
|
-
timeout: number;
|
|
48
|
-
method?: 'notifyWhenChangesStop' | ((callback: () => void, timeout: number) => () => void);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
registerExtender('notify', (target, notifyWhen) => {
|
|
52
|
-
target.equalityComparer = notifyWhen === 'always'
|
|
53
|
-
? undefined
|
|
54
|
-
: valuesArePrimitiveAndEqual;
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
registerExtender('rateLimit', (target, options) => {
|
|
58
|
-
let timeout: number;
|
|
59
|
-
let method: RateLimitOptions['method'] | undefined;
|
|
60
|
-
|
|
61
|
-
if (typeof options === 'number') {
|
|
62
|
-
timeout = options;
|
|
63
|
-
} else {
|
|
64
|
-
const opts = options as RateLimitOptions;
|
|
65
|
-
timeout = opts.timeout;
|
|
66
|
-
method = opts.method;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
target._deferUpdates = false;
|
|
70
|
-
|
|
71
|
-
const limitFunction = typeof method === 'function'
|
|
72
|
-
? method
|
|
73
|
-
: method === 'notifyWhenChangesStop' ? debounce : throttle;
|
|
74
|
-
|
|
75
|
-
target.limit((callback) => limitFunction(callback, timeout));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
registerExtender('deferred', (target, options) => {
|
|
79
|
-
if (options !== true) {
|
|
80
|
-
throw new Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!target._deferUpdates) {
|
|
84
|
-
target._deferUpdates = true;
|
|
85
|
-
target.limit((callback) => {
|
|
86
|
-
let handle: number;
|
|
87
|
-
let ignoreUpdates = false;
|
|
88
|
-
return () => {
|
|
89
|
-
if (!ignoreUpdates) {
|
|
90
|
-
cancel(handle);
|
|
91
|
-
handle = schedule(callback);
|
|
92
|
-
try {
|
|
93
|
-
ignoreUpdates = true;
|
|
94
|
-
target.notifySubscribers(undefined as never, 'dirty');
|
|
95
|
-
} finally {
|
|
96
|
-
ignoreUpdates = false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
});
|
|
1
|
+
import type { Subscribable } from './subscribable.js';
|
|
2
|
+
import { valuesArePrimitiveAndEqual } from './subscribable.js';
|
|
3
|
+
import { schedule, cancel } from './tasks.js';
|
|
4
|
+
|
|
5
|
+
export type ExtenderHandler = (target: Subscribable, value: unknown) => Subscribable | void;
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
8
|
+
export interface ExtenderMap {
|
|
9
|
+
notify: 'always';
|
|
10
|
+
rateLimit: number | RateLimitOptions;
|
|
11
|
+
deferred: true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ExtenderOptions = Partial<ExtenderMap> & Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const registry: Record<string, ExtenderHandler> = {};
|
|
17
|
+
|
|
18
|
+
export function registerExtender(name: string, handler: ExtenderHandler): void {
|
|
19
|
+
registry[name] = handler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getExtenderHandler(name: string): ExtenderHandler | undefined {
|
|
23
|
+
return registry[name];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function throttle(callback: () => void, timeout: number): () => void {
|
|
27
|
+
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
28
|
+
return function () {
|
|
29
|
+
if (!timeoutInstance) {
|
|
30
|
+
timeoutInstance = setTimeout(function () {
|
|
31
|
+
timeoutInstance = undefined;
|
|
32
|
+
callback();
|
|
33
|
+
}, timeout);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function debounce(callback: () => void, timeout: number): () => void {
|
|
39
|
+
let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
|
|
40
|
+
return function () {
|
|
41
|
+
clearTimeout(timeoutInstance);
|
|
42
|
+
timeoutInstance = setTimeout(callback, timeout);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RateLimitOptions {
|
|
47
|
+
timeout: number;
|
|
48
|
+
method?: 'notifyWhenChangesStop' | ((callback: () => void, timeout: number) => () => void);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerExtender('notify', (target, notifyWhen) => {
|
|
52
|
+
target.equalityComparer = notifyWhen === 'always'
|
|
53
|
+
? undefined
|
|
54
|
+
: valuesArePrimitiveAndEqual;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
registerExtender('rateLimit', (target, options) => {
|
|
58
|
+
let timeout: number;
|
|
59
|
+
let method: RateLimitOptions['method'] | undefined;
|
|
60
|
+
|
|
61
|
+
if (typeof options === 'number') {
|
|
62
|
+
timeout = options;
|
|
63
|
+
} else {
|
|
64
|
+
const opts = options as RateLimitOptions;
|
|
65
|
+
timeout = opts.timeout;
|
|
66
|
+
method = opts.method;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
target._deferUpdates = false;
|
|
70
|
+
|
|
71
|
+
const limitFunction = typeof method === 'function'
|
|
72
|
+
? method
|
|
73
|
+
: method === 'notifyWhenChangesStop' ? debounce : throttle;
|
|
74
|
+
|
|
75
|
+
target.limit((callback) => limitFunction(callback, timeout));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
registerExtender('deferred', (target, options) => {
|
|
79
|
+
if (options !== true) {
|
|
80
|
+
throw new Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!target._deferUpdates) {
|
|
84
|
+
target._deferUpdates = true;
|
|
85
|
+
target.limit((callback) => {
|
|
86
|
+
let handle: number;
|
|
87
|
+
let ignoreUpdates = false;
|
|
88
|
+
return () => {
|
|
89
|
+
if (!ignoreUpdates) {
|
|
90
|
+
cancel(handle);
|
|
91
|
+
handle = schedule(callback);
|
|
92
|
+
try {
|
|
93
|
+
ignoreUpdates = true;
|
|
94
|
+
target.notifySubscribers(undefined as never, 'dirty');
|
|
95
|
+
} finally {
|
|
96
|
+
ignoreUpdates = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|