@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,130 +1,130 @@
|
|
|
1
|
-
import { addNodePreprocessor } from './bindingProvider.js';
|
|
2
|
-
import type { NodePreprocessFn } from './bindingProvider.js';
|
|
3
|
-
import { options } from './options.js';
|
|
4
|
-
|
|
5
|
-
// ---- Interpolation Parser ----
|
|
6
|
-
|
|
7
|
-
// Parses text containing {{ }} markup into alternating plain-text and expression segments.
|
|
8
|
-
// Uses a recursive inside-out approach: finds the outermost {{ and }} pair, then
|
|
9
|
-
// recursively parses the inner text to handle nested {{ }} correctly.
|
|
10
|
-
export function parseInterpolationMarkup(
|
|
11
|
-
textToParse: string,
|
|
12
|
-
outerTextCallback: (text: string) => void,
|
|
13
|
-
expressionCallback: (expression: string) => void,
|
|
14
|
-
): void {
|
|
15
|
-
function innerParse(text: string): void {
|
|
16
|
-
const innerMatch = text.match(/^([\s\S]*)}}([\s\S]*?)\{\{([\s\S]*)$/);
|
|
17
|
-
if (innerMatch) {
|
|
18
|
-
innerParse(innerMatch[1]);
|
|
19
|
-
outerTextCallback(innerMatch[2]);
|
|
20
|
-
expressionCallback(innerMatch[3]);
|
|
21
|
-
} else {
|
|
22
|
-
expressionCallback(text);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const outerMatch = textToParse.match(/^([\s\S]*?)\{\{([\s\S]*)}}([\s\S]*)$/);
|
|
27
|
-
if (outerMatch) {
|
|
28
|
-
outerTextCallback(outerMatch[1]);
|
|
29
|
-
innerParse(outerMatch[2]);
|
|
30
|
-
outerTextCallback(outerMatch[3]);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function trim(s: string | null | undefined): string {
|
|
35
|
-
return s == null ? '' : s.trim();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---- Expression → Comment Nodes ----
|
|
39
|
-
|
|
40
|
-
// Converts a parsed expression into comment-based binding nodes.
|
|
41
|
-
// {{ expr }} → <!-- tap text: expr --><!-- /tap -->
|
|
42
|
-
// {{{ expr }}} → <!-- tap html: expr --><!-- /tap -->
|
|
43
|
-
// {{# if cond }} → <!-- tap if: cond -->
|
|
44
|
-
// {{# if cond /}} → <!-- tap if: cond --><!-- /tap --> (self-closing)
|
|
45
|
-
// {{/ }} → <!-- /tap -->
|
|
46
|
-
export function wrapExpression(expressionText: string, node?: Node): Node[] {
|
|
47
|
-
const ownerDocument = node?.ownerDocument ?? document;
|
|
48
|
-
let closeComment = true;
|
|
49
|
-
let binding: string | undefined;
|
|
50
|
-
expressionText = trim(expressionText);
|
|
51
|
-
|
|
52
|
-
const firstChar = expressionText[0];
|
|
53
|
-
const lastChar = expressionText[expressionText.length - 1];
|
|
54
|
-
const result: Node[] = [];
|
|
55
|
-
|
|
56
|
-
if (firstChar === '#') {
|
|
57
|
-
if (lastChar === '/') {
|
|
58
|
-
binding = trim(expressionText.slice(1, -1));
|
|
59
|
-
} else {
|
|
60
|
-
binding = trim(expressionText.slice(1));
|
|
61
|
-
closeComment = false;
|
|
62
|
-
}
|
|
63
|
-
const matches = binding.match(/^([^,"'{}()\/:[\]\s]+)\s+([^\s:].*)/);
|
|
64
|
-
if (matches) {
|
|
65
|
-
binding = matches[1] + ':' + matches[2];
|
|
66
|
-
}
|
|
67
|
-
} else if (firstChar === '/') {
|
|
68
|
-
// Closing tag — just emit the end comment
|
|
69
|
-
} else if (firstChar === '{' && lastChar === '}') {
|
|
70
|
-
binding = 'html:' + trim(expressionText.slice(1, -1));
|
|
71
|
-
} else {
|
|
72
|
-
binding = 'text:' + trim(expressionText);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (binding) {
|
|
76
|
-
result.push(ownerDocument.createComment(' tap ' + binding + ' '));
|
|
77
|
-
}
|
|
78
|
-
if (closeComment) {
|
|
79
|
-
result.push(ownerDocument.createComment(' /tap '));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---- Node Preprocessor ----
|
|
86
|
-
|
|
87
|
-
export const interpolationMarkupPreprocessor: NodePreprocessFn = function (node: Node): Node[] | void {
|
|
88
|
-
if (
|
|
89
|
-
node.nodeType !== 3 ||
|
|
90
|
-
!node.nodeValue ||
|
|
91
|
-
node.nodeValue.indexOf('{{') === -1 ||
|
|
92
|
-
(node.parentNode && (node.parentNode as Element).nodeName === 'TEXTAREA')
|
|
93
|
-
) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const nodes: Node[] = [];
|
|
98
|
-
|
|
99
|
-
function addTextNode(text: string): void {
|
|
100
|
-
if (text) {
|
|
101
|
-
nodes.push(node.ownerDocument!.createTextNode(text));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function addExpressionNodes(expressionText: string): void {
|
|
106
|
-
if (expressionText) {
|
|
107
|
-
nodes.push(...wrapExpression(expressionText, node));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
parseInterpolationMarkup(node.nodeValue, addTextNode, addExpressionNodes);
|
|
112
|
-
|
|
113
|
-
if (nodes.length) {
|
|
114
|
-
if (node.parentNode) {
|
|
115
|
-
const parent = node.parentNode;
|
|
116
|
-
for (const newNode of nodes) {
|
|
117
|
-
parent.insertBefore(newNode, node);
|
|
118
|
-
}
|
|
119
|
-
parent.removeChild(node);
|
|
120
|
-
}
|
|
121
|
-
return nodes;
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// ---- Enable ----
|
|
126
|
-
|
|
127
|
-
export function enableInterpolationMarkup(): void {
|
|
128
|
-
options.interpolation = true;
|
|
129
|
-
addNodePreprocessor(interpolationMarkupPreprocessor);
|
|
130
|
-
}
|
|
1
|
+
import { addNodePreprocessor } from './bindingProvider.js';
|
|
2
|
+
import type { NodePreprocessFn } from './bindingProvider.js';
|
|
3
|
+
import { options } from './options.js';
|
|
4
|
+
|
|
5
|
+
// ---- Interpolation Parser ----
|
|
6
|
+
|
|
7
|
+
// Parses text containing {{ }} markup into alternating plain-text and expression segments.
|
|
8
|
+
// Uses a recursive inside-out approach: finds the outermost {{ and }} pair, then
|
|
9
|
+
// recursively parses the inner text to handle nested {{ }} correctly.
|
|
10
|
+
export function parseInterpolationMarkup(
|
|
11
|
+
textToParse: string,
|
|
12
|
+
outerTextCallback: (text: string) => void,
|
|
13
|
+
expressionCallback: (expression: string) => void,
|
|
14
|
+
): void {
|
|
15
|
+
function innerParse(text: string): void {
|
|
16
|
+
const innerMatch = text.match(/^([\s\S]*)}}([\s\S]*?)\{\{([\s\S]*)$/);
|
|
17
|
+
if (innerMatch) {
|
|
18
|
+
innerParse(innerMatch[1]);
|
|
19
|
+
outerTextCallback(innerMatch[2]);
|
|
20
|
+
expressionCallback(innerMatch[3]);
|
|
21
|
+
} else {
|
|
22
|
+
expressionCallback(text);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const outerMatch = textToParse.match(/^([\s\S]*?)\{\{([\s\S]*)}}([\s\S]*)$/);
|
|
27
|
+
if (outerMatch) {
|
|
28
|
+
outerTextCallback(outerMatch[1]);
|
|
29
|
+
innerParse(outerMatch[2]);
|
|
30
|
+
outerTextCallback(outerMatch[3]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function trim(s: string | null | undefined): string {
|
|
35
|
+
return s == null ? '' : s.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- Expression → Comment Nodes ----
|
|
39
|
+
|
|
40
|
+
// Converts a parsed expression into comment-based binding nodes.
|
|
41
|
+
// {{ expr }} → <!-- tap text: expr --><!-- /tap -->
|
|
42
|
+
// {{{ expr }}} → <!-- tap html: expr --><!-- /tap -->
|
|
43
|
+
// {{# if cond }} → <!-- tap if: cond -->
|
|
44
|
+
// {{# if cond /}} → <!-- tap if: cond --><!-- /tap --> (self-closing)
|
|
45
|
+
// {{/ }} → <!-- /tap -->
|
|
46
|
+
export function wrapExpression(expressionText: string, node?: Node): Node[] {
|
|
47
|
+
const ownerDocument = node?.ownerDocument ?? document;
|
|
48
|
+
let closeComment = true;
|
|
49
|
+
let binding: string | undefined;
|
|
50
|
+
expressionText = trim(expressionText);
|
|
51
|
+
|
|
52
|
+
const firstChar = expressionText[0];
|
|
53
|
+
const lastChar = expressionText[expressionText.length - 1];
|
|
54
|
+
const result: Node[] = [];
|
|
55
|
+
|
|
56
|
+
if (firstChar === '#') {
|
|
57
|
+
if (lastChar === '/') {
|
|
58
|
+
binding = trim(expressionText.slice(1, -1));
|
|
59
|
+
} else {
|
|
60
|
+
binding = trim(expressionText.slice(1));
|
|
61
|
+
closeComment = false;
|
|
62
|
+
}
|
|
63
|
+
const matches = binding.match(/^([^,"'{}()\/:[\]\s]+)\s+([^\s:].*)/);
|
|
64
|
+
if (matches) {
|
|
65
|
+
binding = matches[1] + ':' + matches[2];
|
|
66
|
+
}
|
|
67
|
+
} else if (firstChar === '/') {
|
|
68
|
+
// Closing tag — just emit the end comment
|
|
69
|
+
} else if (firstChar === '{' && lastChar === '}') {
|
|
70
|
+
binding = 'html:' + trim(expressionText.slice(1, -1));
|
|
71
|
+
} else {
|
|
72
|
+
binding = 'text:' + trim(expressionText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (binding) {
|
|
76
|
+
result.push(ownerDocument.createComment(' tap ' + binding + ' '));
|
|
77
|
+
}
|
|
78
|
+
if (closeComment) {
|
|
79
|
+
result.push(ownerDocument.createComment(' /tap '));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- Node Preprocessor ----
|
|
86
|
+
|
|
87
|
+
export const interpolationMarkupPreprocessor: NodePreprocessFn = function (node: Node): Node[] | void {
|
|
88
|
+
if (
|
|
89
|
+
node.nodeType !== 3 ||
|
|
90
|
+
!node.nodeValue ||
|
|
91
|
+
node.nodeValue.indexOf('{{') === -1 ||
|
|
92
|
+
(node.parentNode && (node.parentNode as Element).nodeName === 'TEXTAREA')
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nodes: Node[] = [];
|
|
98
|
+
|
|
99
|
+
function addTextNode(text: string): void {
|
|
100
|
+
if (text) {
|
|
101
|
+
nodes.push(node.ownerDocument!.createTextNode(text));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addExpressionNodes(expressionText: string): void {
|
|
106
|
+
if (expressionText) {
|
|
107
|
+
nodes.push(...wrapExpression(expressionText, node));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
parseInterpolationMarkup(node.nodeValue, addTextNode, addExpressionNodes);
|
|
112
|
+
|
|
113
|
+
if (nodes.length) {
|
|
114
|
+
if (node.parentNode) {
|
|
115
|
+
const parent = node.parentNode;
|
|
116
|
+
for (const newNode of nodes) {
|
|
117
|
+
parent.insertBefore(newNode, node);
|
|
118
|
+
}
|
|
119
|
+
parent.removeChild(node);
|
|
120
|
+
}
|
|
121
|
+
return nodes;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---- Enable ----
|
|
126
|
+
|
|
127
|
+
export function enableInterpolationMarkup(): void {
|
|
128
|
+
options.interpolation = true;
|
|
129
|
+
addNodePreprocessor(interpolationMarkupPreprocessor);
|
|
130
|
+
}
|
package/src/memoization.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
type MemoCallback = (...args: unknown[]) => void;
|
|
2
|
-
|
|
3
|
-
const memos = new Map<string, MemoCallback>();
|
|
4
|
-
|
|
5
|
-
function randomMax8HexChars(): string {
|
|
6
|
-
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function generateRandomId(): string {
|
|
10
|
-
return randomMax8HexChars() + randomMax8HexChars();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function findMemoNodes(
|
|
14
|
-
rootNode: Node | null,
|
|
15
|
-
appendToArray: { domNode: Comment; memoId: string }[],
|
|
16
|
-
): void {
|
|
17
|
-
if (!rootNode) return;
|
|
18
|
-
if (rootNode.nodeType === 8) {
|
|
19
|
-
const memoId = parseMemoText(rootNode.nodeValue || '');
|
|
20
|
-
if (memoId != null)
|
|
21
|
-
appendToArray.push({ domNode: rootNode as Comment, memoId });
|
|
22
|
-
} else if (rootNode.nodeType === 1) {
|
|
23
|
-
const childNodes = rootNode.childNodes;
|
|
24
|
-
for (let i = 0; i < childNodes.length; i++)
|
|
25
|
-
findMemoNodes(childNodes[i], appendToArray);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function memoize(callback: MemoCallback): string {
|
|
30
|
-
if (typeof callback !== 'function')
|
|
31
|
-
throw new Error('You can only pass a function to memoize()');
|
|
32
|
-
const memoId = generateRandomId();
|
|
33
|
-
memos.set(memoId, callback);
|
|
34
|
-
return '<!--[tap_memo:' + memoId + ']-->';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function unmemoize(memoId: string, callbackParams?: unknown[]): boolean {
|
|
38
|
-
const callback = memos.get(memoId);
|
|
39
|
-
if (callback === undefined)
|
|
40
|
-
throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
|
|
41
|
-
try {
|
|
42
|
-
callback.apply(null, callbackParams || []);
|
|
43
|
-
return true;
|
|
44
|
-
} finally {
|
|
45
|
-
memos.delete(memoId);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function unmemoizeDomNodeAndDescendants(
|
|
50
|
-
domNode: Node,
|
|
51
|
-
extraCallbackParamsArray?: unknown[],
|
|
52
|
-
): void {
|
|
53
|
-
const found: { domNode: Comment; memoId: string }[] = [];
|
|
54
|
-
findMemoNodes(domNode, found);
|
|
55
|
-
for (const { domNode: node, memoId } of found) {
|
|
56
|
-
const combinedParams: unknown[] = [node];
|
|
57
|
-
if (extraCallbackParamsArray)
|
|
58
|
-
combinedParams.push(...extraCallbackParamsArray);
|
|
59
|
-
unmemoize(memoId, combinedParams);
|
|
60
|
-
node.nodeValue = '';
|
|
61
|
-
if (node.parentNode)
|
|
62
|
-
node.parentNode.removeChild(node);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const memoTextRegex = /^\[tap_memo:(.*?)\]$/;
|
|
67
|
-
|
|
68
|
-
export function parseMemoText(memoText: string): string | null {
|
|
69
|
-
const match = memoText.match(memoTextRegex);
|
|
70
|
-
return match ? match[1] : null;
|
|
71
|
-
}
|
|
1
|
+
type MemoCallback = (...args: unknown[]) => void;
|
|
2
|
+
|
|
3
|
+
const memos = new Map<string, MemoCallback>();
|
|
4
|
+
|
|
5
|
+
function randomMax8HexChars(): string {
|
|
6
|
+
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function generateRandomId(): string {
|
|
10
|
+
return randomMax8HexChars() + randomMax8HexChars();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function findMemoNodes(
|
|
14
|
+
rootNode: Node | null,
|
|
15
|
+
appendToArray: { domNode: Comment; memoId: string }[],
|
|
16
|
+
): void {
|
|
17
|
+
if (!rootNode) return;
|
|
18
|
+
if (rootNode.nodeType === 8) {
|
|
19
|
+
const memoId = parseMemoText(rootNode.nodeValue || '');
|
|
20
|
+
if (memoId != null)
|
|
21
|
+
appendToArray.push({ domNode: rootNode as Comment, memoId });
|
|
22
|
+
} else if (rootNode.nodeType === 1) {
|
|
23
|
+
const childNodes = rootNode.childNodes;
|
|
24
|
+
for (let i = 0; i < childNodes.length; i++)
|
|
25
|
+
findMemoNodes(childNodes[i], appendToArray);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function memoize(callback: MemoCallback): string {
|
|
30
|
+
if (typeof callback !== 'function')
|
|
31
|
+
throw new Error('You can only pass a function to memoize()');
|
|
32
|
+
const memoId = generateRandomId();
|
|
33
|
+
memos.set(memoId, callback);
|
|
34
|
+
return '<!--[tap_memo:' + memoId + ']-->';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function unmemoize(memoId: string, callbackParams?: unknown[]): boolean {
|
|
38
|
+
const callback = memos.get(memoId);
|
|
39
|
+
if (callback === undefined)
|
|
40
|
+
throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
|
|
41
|
+
try {
|
|
42
|
+
callback.apply(null, callbackParams || []);
|
|
43
|
+
return true;
|
|
44
|
+
} finally {
|
|
45
|
+
memos.delete(memoId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function unmemoizeDomNodeAndDescendants(
|
|
50
|
+
domNode: Node,
|
|
51
|
+
extraCallbackParamsArray?: unknown[],
|
|
52
|
+
): void {
|
|
53
|
+
const found: { domNode: Comment; memoId: string }[] = [];
|
|
54
|
+
findMemoNodes(domNode, found);
|
|
55
|
+
for (const { domNode: node, memoId } of found) {
|
|
56
|
+
const combinedParams: unknown[] = [node];
|
|
57
|
+
if (extraCallbackParamsArray)
|
|
58
|
+
combinedParams.push(...extraCallbackParamsArray);
|
|
59
|
+
unmemoize(memoId, combinedParams);
|
|
60
|
+
node.nodeValue = '';
|
|
61
|
+
if (node.parentNode)
|
|
62
|
+
node.parentNode.removeChild(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const memoTextRegex = /^\[tap_memo:(.*?)\]$/;
|
|
67
|
+
|
|
68
|
+
export function parseMemoText(memoText: string): string | null {
|
|
69
|
+
const match = memoText.match(memoTextRegex);
|
|
70
|
+
return match ? match[1] : null;
|
|
71
|
+
}
|
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
import {
|
|
2
|
-
bindingHandlers,
|
|
3
|
-
getBindingHandler,
|
|
4
|
-
addBindingHandlerCreator,
|
|
5
|
-
addBindingPreprocessor,
|
|
6
|
-
} from './bindingProvider.js';
|
|
7
|
-
import type { BindingHandler, PreprocessFn } from './bindingProvider.js';
|
|
8
|
-
import { allowedVirtualElementBindings } from './virtualElements.js';
|
|
9
|
-
import { parseObjectLiteral } from './expressionRewriting.js';
|
|
10
|
-
import type { BindingContext } from './bindingContext.js';
|
|
11
|
-
import { options } from './options.js';
|
|
12
|
-
import type { AllBindingsAccessor } from './expressionRewriting.js';
|
|
13
|
-
|
|
14
|
-
const NAMESPACE_DIVIDER = '.';
|
|
15
|
-
const NAMESPACED_BINDING_REGEX = /^([^.]+)\.(.+)$/;
|
|
16
|
-
|
|
17
|
-
// Translates `namespace.name: value` into `namespace: {name: value}`.
|
|
18
|
-
// This works because attr/css/style/event all accept an object map.
|
|
19
|
-
export function defaultGetNamespacedHandler(
|
|
20
|
-
this: BindingHandler,
|
|
21
|
-
name: string,
|
|
22
|
-
namespace: string,
|
|
23
|
-
namespacedName: string,
|
|
24
|
-
): BindingHandler {
|
|
25
|
-
const handler: BindingHandler = Object.assign({}, this);
|
|
26
|
-
|
|
27
|
-
function makeSubValueAccessor(valueAccessor: () => unknown): () => Record<string, unknown> {
|
|
28
|
-
return () => {
|
|
29
|
-
const result: Record<string, unknown> = {};
|
|
30
|
-
result[name] = valueAccessor();
|
|
31
|
-
return result;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (handler.init) {
|
|
36
|
-
handler.init = function (
|
|
37
|
-
element: Node,
|
|
38
|
-
valueAccessor: () => unknown,
|
|
39
|
-
allBindings: AllBindingsAccessor,
|
|
40
|
-
viewModel: unknown,
|
|
41
|
-
bindingContext: BindingContext,
|
|
42
|
-
) {
|
|
43
|
-
return bindingHandlers[namespace].init!.call(
|
|
44
|
-
this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (handler.update) {
|
|
50
|
-
handler.update = function (
|
|
51
|
-
element: Node,
|
|
52
|
-
valueAccessor: () => unknown,
|
|
53
|
-
allBindings: AllBindingsAccessor,
|
|
54
|
-
viewModel: unknown,
|
|
55
|
-
bindingContext: BindingContext,
|
|
56
|
-
) {
|
|
57
|
-
bindingHandlers[namespace].update!.call(
|
|
58
|
-
this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
handler.preprocess = undefined;
|
|
64
|
-
|
|
65
|
-
if (allowedVirtualElementBindings[namespace]) {
|
|
66
|
-
allowedVirtualElementBindings[namespacedName] = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return handler;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Register the dynamic handler creator for the `namespace.name` pattern.
|
|
73
|
-
// When `getBindingHandler('attr.href')` is called and no explicit handler exists,
|
|
74
|
-
// this splits on `.`, finds the `attr` handler, and creates a derived handler
|
|
75
|
-
// that wraps the single value into the object map format the namespace handler expects.
|
|
76
|
-
export function enableNamespacedBindings(): void {
|
|
77
|
-
options.namespacedBindings = true;
|
|
78
|
-
addBindingHandlerCreator(NAMESPACED_BINDING_REGEX, (match, bindingKey) => {
|
|
79
|
-
const namespace = match[1];
|
|
80
|
-
const namespaceHandler = bindingHandlers[namespace];
|
|
81
|
-
if (namespaceHandler) {
|
|
82
|
-
const name = match[2];
|
|
83
|
-
const handlerFn = namespaceHandler.getNamespacedHandler || defaultGetNamespacedHandler;
|
|
84
|
-
const handler = handlerFn.call(namespaceHandler, name, namespace, bindingKey);
|
|
85
|
-
bindingHandlers[bindingKey] = handler;
|
|
86
|
-
return handler;
|
|
87
|
-
}
|
|
88
|
-
return undefined;
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Preprocessor that auto-expands `binding: {key: val, key2: val2}` into
|
|
93
|
-
// individual `binding.key: val` and `binding.key2: val2` bindings.
|
|
94
|
-
export const autoNamespacedPreprocessor: PreprocessFn = function (
|
|
95
|
-
value: string,
|
|
96
|
-
binding: string,
|
|
97
|
-
addBinding: (key: string, val: string) => void,
|
|
98
|
-
): string | void {
|
|
99
|
-
if (value.charAt(0) !== '{') return value;
|
|
100
|
-
|
|
101
|
-
const subBindings = parseObjectLiteral(value);
|
|
102
|
-
for (const kv of subBindings) {
|
|
103
|
-
addBinding(binding + NAMESPACE_DIVIDER + (kv.key || kv.unknown || ''), kv.value || '');
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Enable the auto-expand preprocessor for a specific binding, so
|
|
108
|
-
// `attr: {href: url, title: tip}` auto-expands to `attr.href: url, attr.title: tip`.
|
|
109
|
-
export function enableAutoNamespacedSyntax(bindingKeyOrHandler: string | BindingHandler): void {
|
|
110
|
-
addBindingPreprocessor(bindingKeyOrHandler, autoNamespacedPreprocessor);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Add a preprocessor to all dynamically-generated namespaced handlers under a namespace.
|
|
114
|
-
// For example, adding a filter preprocessor to 'attr' would make it apply to
|
|
115
|
-
// every `attr.x` binding that gets created.
|
|
116
|
-
export function addDefaultNamespacedBindingPreprocessor(
|
|
117
|
-
namespace: string,
|
|
118
|
-
preprocessFn: PreprocessFn,
|
|
119
|
-
): void {
|
|
120
|
-
const handler = getBindingHandler(namespace);
|
|
121
|
-
if (handler) {
|
|
122
|
-
const previousHandlerFn = handler.getNamespacedHandler || defaultGetNamespacedHandler;
|
|
123
|
-
handler.getNamespacedHandler = function (
|
|
124
|
-
name: string,
|
|
125
|
-
ns: string,
|
|
126
|
-
namespacedName: string,
|
|
127
|
-
): BindingHandler {
|
|
128
|
-
const created = previousHandlerFn.call(this, name, ns, namespacedName);
|
|
129
|
-
return addBindingPreprocessor(created, preprocessFn);
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
bindingHandlers,
|
|
3
|
+
getBindingHandler,
|
|
4
|
+
addBindingHandlerCreator,
|
|
5
|
+
addBindingPreprocessor,
|
|
6
|
+
} from './bindingProvider.js';
|
|
7
|
+
import type { BindingHandler, PreprocessFn } from './bindingProvider.js';
|
|
8
|
+
import { allowedVirtualElementBindings } from './virtualElements.js';
|
|
9
|
+
import { parseObjectLiteral } from './expressionRewriting.js';
|
|
10
|
+
import type { BindingContext } from './bindingContext.js';
|
|
11
|
+
import { options } from './options.js';
|
|
12
|
+
import type { AllBindingsAccessor } from './expressionRewriting.js';
|
|
13
|
+
|
|
14
|
+
const NAMESPACE_DIVIDER = '.';
|
|
15
|
+
const NAMESPACED_BINDING_REGEX = /^([^.]+)\.(.+)$/;
|
|
16
|
+
|
|
17
|
+
// Translates `namespace.name: value` into `namespace: {name: value}`.
|
|
18
|
+
// This works because attr/css/style/event all accept an object map.
|
|
19
|
+
export function defaultGetNamespacedHandler(
|
|
20
|
+
this: BindingHandler,
|
|
21
|
+
name: string,
|
|
22
|
+
namespace: string,
|
|
23
|
+
namespacedName: string,
|
|
24
|
+
): BindingHandler {
|
|
25
|
+
const handler: BindingHandler = Object.assign({}, this);
|
|
26
|
+
|
|
27
|
+
function makeSubValueAccessor(valueAccessor: () => unknown): () => Record<string, unknown> {
|
|
28
|
+
return () => {
|
|
29
|
+
const result: Record<string, unknown> = {};
|
|
30
|
+
result[name] = valueAccessor();
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (handler.init) {
|
|
36
|
+
handler.init = function (
|
|
37
|
+
element: Node,
|
|
38
|
+
valueAccessor: () => unknown,
|
|
39
|
+
allBindings: AllBindingsAccessor,
|
|
40
|
+
viewModel: unknown,
|
|
41
|
+
bindingContext: BindingContext,
|
|
42
|
+
) {
|
|
43
|
+
return bindingHandlers[namespace].init!.call(
|
|
44
|
+
this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (handler.update) {
|
|
50
|
+
handler.update = function (
|
|
51
|
+
element: Node,
|
|
52
|
+
valueAccessor: () => unknown,
|
|
53
|
+
allBindings: AllBindingsAccessor,
|
|
54
|
+
viewModel: unknown,
|
|
55
|
+
bindingContext: BindingContext,
|
|
56
|
+
) {
|
|
57
|
+
bindingHandlers[namespace].update!.call(
|
|
58
|
+
this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
handler.preprocess = undefined;
|
|
64
|
+
|
|
65
|
+
if (allowedVirtualElementBindings[namespace]) {
|
|
66
|
+
allowedVirtualElementBindings[namespacedName] = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return handler;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Register the dynamic handler creator for the `namespace.name` pattern.
|
|
73
|
+
// When `getBindingHandler('attr.href')` is called and no explicit handler exists,
|
|
74
|
+
// this splits on `.`, finds the `attr` handler, and creates a derived handler
|
|
75
|
+
// that wraps the single value into the object map format the namespace handler expects.
|
|
76
|
+
export function enableNamespacedBindings(): void {
|
|
77
|
+
options.namespacedBindings = true;
|
|
78
|
+
addBindingHandlerCreator(NAMESPACED_BINDING_REGEX, (match, bindingKey) => {
|
|
79
|
+
const namespace = match[1];
|
|
80
|
+
const namespaceHandler = bindingHandlers[namespace];
|
|
81
|
+
if (namespaceHandler) {
|
|
82
|
+
const name = match[2];
|
|
83
|
+
const handlerFn = namespaceHandler.getNamespacedHandler || defaultGetNamespacedHandler;
|
|
84
|
+
const handler = handlerFn.call(namespaceHandler, name, namespace, bindingKey);
|
|
85
|
+
bindingHandlers[bindingKey] = handler;
|
|
86
|
+
return handler;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Preprocessor that auto-expands `binding: {key: val, key2: val2}` into
|
|
93
|
+
// individual `binding.key: val` and `binding.key2: val2` bindings.
|
|
94
|
+
export const autoNamespacedPreprocessor: PreprocessFn = function (
|
|
95
|
+
value: string,
|
|
96
|
+
binding: string,
|
|
97
|
+
addBinding: (key: string, val: string) => void,
|
|
98
|
+
): string | void {
|
|
99
|
+
if (value.charAt(0) !== '{') return value;
|
|
100
|
+
|
|
101
|
+
const subBindings = parseObjectLiteral(value);
|
|
102
|
+
for (const kv of subBindings) {
|
|
103
|
+
addBinding(binding + NAMESPACE_DIVIDER + (kv.key || kv.unknown || ''), kv.value || '');
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Enable the auto-expand preprocessor for a specific binding, so
|
|
108
|
+
// `attr: {href: url, title: tip}` auto-expands to `attr.href: url, attr.title: tip`.
|
|
109
|
+
export function enableAutoNamespacedSyntax(bindingKeyOrHandler: string | BindingHandler): void {
|
|
110
|
+
addBindingPreprocessor(bindingKeyOrHandler, autoNamespacedPreprocessor);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add a preprocessor to all dynamically-generated namespaced handlers under a namespace.
|
|
114
|
+
// For example, adding a filter preprocessor to 'attr' would make it apply to
|
|
115
|
+
// every `attr.x` binding that gets created.
|
|
116
|
+
export function addDefaultNamespacedBindingPreprocessor(
|
|
117
|
+
namespace: string,
|
|
118
|
+
preprocessFn: PreprocessFn,
|
|
119
|
+
): void {
|
|
120
|
+
const handler = getBindingHandler(namespace);
|
|
121
|
+
if (handler) {
|
|
122
|
+
const previousHandlerFn = handler.getNamespacedHandler || defaultGetNamespacedHandler;
|
|
123
|
+
handler.getNamespacedHandler = function (
|
|
124
|
+
name: string,
|
|
125
|
+
ns: string,
|
|
126
|
+
namespacedName: string,
|
|
127
|
+
): BindingHandler {
|
|
128
|
+
const created = previousHandlerFn.call(this, name, ns, namespacedName);
|
|
129
|
+
return addBindingPreprocessor(created, preprocessFn);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|