@esportsplus/template 0.16.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.
Files changed (90) hide show
  1. package/.editorconfig +9 -0
  2. package/.gitattributes +2 -0
  3. package/.github/dependabot.yml +25 -0
  4. package/.github/workflows/bump.yml +9 -0
  5. package/.github/workflows/dependabot.yml +12 -0
  6. package/.github/workflows/publish.yml +16 -0
  7. package/README.md +385 -0
  8. package/build/attributes.d.ts +5 -0
  9. package/build/attributes.js +212 -0
  10. package/build/compiler/codegen.d.ts +21 -0
  11. package/build/compiler/codegen.js +303 -0
  12. package/build/compiler/constants.d.ts +16 -0
  13. package/build/compiler/constants.js +19 -0
  14. package/build/compiler/index.d.ts +14 -0
  15. package/build/compiler/index.js +61 -0
  16. package/build/compiler/parser.d.ts +19 -0
  17. package/build/compiler/parser.js +164 -0
  18. package/build/compiler/plugins/tsc.d.ts +3 -0
  19. package/build/compiler/plugins/tsc.js +4 -0
  20. package/build/compiler/plugins/vite.d.ts +13 -0
  21. package/build/compiler/plugins/vite.js +8 -0
  22. package/build/compiler/ts-analyzer.d.ts +4 -0
  23. package/build/compiler/ts-analyzer.js +63 -0
  24. package/build/compiler/ts-parser.d.ts +24 -0
  25. package/build/compiler/ts-parser.js +67 -0
  26. package/build/constants.d.ts +12 -0
  27. package/build/constants.js +25 -0
  28. package/build/event/index.d.ts +10 -0
  29. package/build/event/index.js +90 -0
  30. package/build/event/onconnect.d.ts +3 -0
  31. package/build/event/onconnect.js +15 -0
  32. package/build/event/onresize.d.ts +3 -0
  33. package/build/event/onresize.js +26 -0
  34. package/build/event/ontick.d.ts +6 -0
  35. package/build/event/ontick.js +41 -0
  36. package/build/html.d.ts +9 -0
  37. package/build/html.js +7 -0
  38. package/build/index.d.ts +8 -0
  39. package/build/index.js +12 -0
  40. package/build/render.d.ts +3 -0
  41. package/build/render.js +8 -0
  42. package/build/slot/array.d.ts +25 -0
  43. package/build/slot/array.js +189 -0
  44. package/build/slot/cleanup.d.ts +4 -0
  45. package/build/slot/cleanup.js +23 -0
  46. package/build/slot/effect.d.ts +12 -0
  47. package/build/slot/effect.js +85 -0
  48. package/build/slot/index.d.ts +7 -0
  49. package/build/slot/index.js +14 -0
  50. package/build/slot/render.d.ts +2 -0
  51. package/build/slot/render.js +44 -0
  52. package/build/svg.d.ts +5 -0
  53. package/build/svg.js +14 -0
  54. package/build/types.d.ts +23 -0
  55. package/build/types.js +1 -0
  56. package/build/utilities.d.ts +7 -0
  57. package/build/utilities.js +31 -0
  58. package/package.json +43 -0
  59. package/src/attributes.ts +313 -0
  60. package/src/compiler/codegen.ts +492 -0
  61. package/src/compiler/constants.ts +25 -0
  62. package/src/compiler/index.ts +87 -0
  63. package/src/compiler/parser.ts +242 -0
  64. package/src/compiler/plugins/tsc.ts +6 -0
  65. package/src/compiler/plugins/vite.ts +10 -0
  66. package/src/compiler/ts-analyzer.ts +89 -0
  67. package/src/compiler/ts-parser.ts +112 -0
  68. package/src/constants.ts +44 -0
  69. package/src/event/index.ts +130 -0
  70. package/src/event/onconnect.ts +22 -0
  71. package/src/event/onresize.ts +37 -0
  72. package/src/event/ontick.ts +59 -0
  73. package/src/html.ts +18 -0
  74. package/src/index.ts +19 -0
  75. package/src/llm.txt +403 -0
  76. package/src/render.ts +13 -0
  77. package/src/slot/array.ts +257 -0
  78. package/src/slot/cleanup.ts +37 -0
  79. package/src/slot/effect.ts +114 -0
  80. package/src/slot/index.ts +17 -0
  81. package/src/slot/render.ts +61 -0
  82. package/src/svg.ts +27 -0
  83. package/src/types.ts +40 -0
  84. package/src/utilities.ts +53 -0
  85. package/storage/compiler-architecture-2026-01-13.md +420 -0
  86. package/test/dist/test.js +1912 -0
  87. package/test/dist/test.js.map +1 -0
  88. package/test/index.ts +648 -0
  89. package/test/vite.config.ts +23 -0
  90. package/tsconfig.json +8 -0
@@ -0,0 +1,242 @@
1
+ import { ATTRIBUTE_DELIMITERS, SLOT_HTML } from '../constants';
2
+ import { PACKAGE_NAME, TYPES } from './constants';
3
+
4
+
5
+ type NodePath = ('firstChild' | 'firstElementChild' | 'nextElementSibling' | 'nextSibling')[];
6
+
7
+
8
+ const NODE_CLOSING = 1;
9
+
10
+ const NODE_COMMENT = 2;
11
+
12
+ const NODE_ELEMENT = 3;
13
+
14
+ const NODE_SLOT = 4;
15
+
16
+ const NODE_VOID = 5;
17
+
18
+ const NODE_WHITELIST: Record<string, number> = {
19
+ '!': NODE_COMMENT,
20
+ '/': NODE_CLOSING
21
+ };
22
+
23
+ const REGEX_CLEANUP_WHITESPACE = /\s+/g;
24
+
25
+ const REGEX_EMPTY_ATTRIBUTES = /\s+[\w:-]+\s*=\s*["']\s*["']|\s+(?=>)/g;
26
+
27
+ const REGEX_EMPTY_TEXT_NODES = /(>|}|\s)\s+(<|{|\s)/g;
28
+
29
+ const REGEX_EVENTS = /(?:\s*on[\w-:]+\s*=(?:\s*["'][^"']*["'])*)/g;
30
+
31
+ const REGEX_SLOT_ATTRIBUTES = /<[\w-]+([^><]*{{\$}}[^><]*)>/g;
32
+
33
+ const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{{\$}}/g;
34
+
35
+ const SLOT_MARKER = '{{$}}';
36
+
37
+
38
+ [
39
+ // html
40
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
41
+ 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
42
+
43
+ // svg
44
+ 'animate', 'animateMotion', 'animateTransform', 'circle', 'ellipse',
45
+ 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix',
46
+ 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood',
47
+ 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMergeNode',
48
+ 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence',
49
+ 'hatch', 'hatchpath', 'image', 'line', 'mpath', 'path', 'polygon', 'polyline',
50
+ 'rect', 'set', 'stop', 'use', 'view'
51
+ ].map(tag => NODE_WHITELIST[tag] = NODE_VOID);
52
+
53
+
54
+ function methods(children: number, copy: NodePath, first: NodePath[number], next: NodePath[number]) {
55
+ let length = copy.length,
56
+ result: NodePath = new Array(length + 1 + children);
57
+
58
+ for (let i = 0, n = length; i < n; i++) {
59
+ result[i] = copy[i];
60
+ }
61
+
62
+ result[length] = first;
63
+
64
+ for (let i = 0, n = children; i < n; i++) {
65
+ result[length + 1 + i] = next;
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+
72
+ const parse = (literals: string[]) => {
73
+ let html = literals
74
+ .join(SLOT_MARKER)
75
+ .replace(REGEX_EMPTY_TEXT_NODES, '$1$2')
76
+ .replace(REGEX_CLEANUP_WHITESPACE, ' ')
77
+ .trim(),
78
+ n = literals.length - 1;
79
+
80
+ if (n === 0) {
81
+ return { html, slots: null };
82
+ }
83
+
84
+ let attributes: Record<string, { names: string[], static: Record<string, string> }> = {},
85
+ buffer = '',
86
+ index = 0,
87
+ level = 0,
88
+ levels = [{ children: 0, elements: 0, path: [] as NodePath }],
89
+ parsed = html.split(SLOT_MARKER),
90
+ slot = 0,
91
+ slots: (
92
+ { path: NodePath; type: TYPES.Node } |
93
+ { attributes: typeof attributes[string]; path: NodePath; type: TYPES.Attribute }
94
+ )[] = [];
95
+
96
+ {
97
+ let attribute = '',
98
+ buffer = '',
99
+ char = '',
100
+ quote = '';
101
+
102
+ for (let match of html.matchAll(REGEX_SLOT_ATTRIBUTES)) {
103
+ let found = match[1];
104
+
105
+ if (attributes[found]) {
106
+ continue;
107
+ }
108
+
109
+ let { names, static: s } = attributes[found] = { names: [], static: {} } as typeof attributes[string];
110
+
111
+ for (let i = 0, n = found.length; i < n; i++) {
112
+ char = found[i];
113
+
114
+ if (char === ' ') {
115
+ if (attribute && attribute in ATTRIBUTE_DELIMITERS) {
116
+ s[attribute] ??= '';
117
+ s[attribute] += `${buffer && s[attribute] ? ATTRIBUTE_DELIMITERS[attribute] : ''}${buffer}`;
118
+ }
119
+
120
+ buffer = '';
121
+ }
122
+ else if (char === '=') {
123
+ attribute = buffer;
124
+ buffer = '';
125
+ }
126
+ else if (char === '"' || char === "'") {
127
+ if (!attribute) {
128
+ continue;
129
+ }
130
+ else if (!quote) {
131
+ quote = char;
132
+ }
133
+ else if (quote === char) {
134
+ if (attribute && attribute in ATTRIBUTE_DELIMITERS) {
135
+ s[attribute] ??= '';
136
+ s[attribute] += `${buffer && s[attribute] ? ATTRIBUTE_DELIMITERS[attribute] : ''}${buffer}`;
137
+ }
138
+
139
+ attribute = '';
140
+ buffer = '';
141
+ quote = '';
142
+ }
143
+ }
144
+ else if (char === '{' && char !== buffer) {
145
+ buffer = char;
146
+ }
147
+ else {
148
+ buffer += char;
149
+
150
+ if (buffer === SLOT_MARKER) {
151
+ buffer = '';
152
+
153
+ if (attribute) {
154
+ names.push(attribute);
155
+
156
+ if (!quote) {
157
+ attribute = '';
158
+ }
159
+ }
160
+ else {
161
+ names.push(TYPES.Attributes);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ {
170
+ for (let match of html.matchAll(REGEX_SLOT_NODES)) {
171
+ let parent = levels[level],
172
+ type = match[1] === undefined ? NODE_SLOT : (
173
+ NODE_WHITELIST[match[1].toLowerCase()] ||
174
+ (match[0].at(-2) === '/' ? NODE_VOID : NODE_ELEMENT)
175
+ );
176
+
177
+ if ((match.index || 1) - 1 > index) {
178
+ parent.children++;
179
+ }
180
+
181
+ if (type === NODE_ELEMENT || type === NODE_VOID) {
182
+ let attr = match[2],
183
+ path = parent.path.length
184
+ ? methods(parent.elements, parent.path, 'firstElementChild', 'nextElementSibling')
185
+ : methods(parent.children, [], 'firstChild', 'nextSibling');
186
+
187
+ if (attr) {
188
+ let attrs = attributes[attr];
189
+
190
+ if (!attrs) {
191
+ throw new Error(`${PACKAGE_NAME}: attribute metadata could not be found for '${attr}'`);
192
+ }
193
+
194
+ slots.push({ attributes: attrs, path, type: TYPES.Attribute });
195
+
196
+ for (let i = 0, n = attrs.names.length; i < n; i++) {
197
+ buffer += parsed[slot++];
198
+ }
199
+ }
200
+
201
+ if (type === NODE_ELEMENT) {
202
+ levels[++level] = { children: 0, elements: 0, path };
203
+ }
204
+
205
+ parent.elements++;
206
+ }
207
+ else if (type === NODE_SLOT) {
208
+ buffer += parsed[slot++] + SLOT_HTML;
209
+ slots.push({
210
+ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'),
211
+ type: TYPES.Node
212
+ });
213
+ }
214
+
215
+ if (n === slot) {
216
+ buffer += parsed[slot];
217
+ break;
218
+ }
219
+
220
+ if (type === NODE_CLOSING) {
221
+ level--;
222
+ }
223
+ else {
224
+ parent.children++;
225
+ }
226
+
227
+ index = (match.index || 0) + match[0].length;
228
+ }
229
+ }
230
+
231
+ buffer = buffer
232
+ .replace(REGEX_EVENTS, '')
233
+ .replace(REGEX_EMPTY_ATTRIBUTES, '');
234
+
235
+ return {
236
+ html: buffer,
237
+ slots: slots.length ? slots : null
238
+ };
239
+ };
240
+
241
+
242
+ export default { parse };
@@ -0,0 +1,6 @@
1
+ import { plugin } from '@esportsplus/typescript/compiler';
2
+ import reactivity from '@esportsplus/reactivity/compiler';
3
+ import template from '..';
4
+
5
+
6
+ export default plugin.tsc([reactivity, template]) as ReturnType<typeof plugin.tsc>;
@@ -0,0 +1,10 @@
1
+ import { plugin } from '@esportsplus/typescript/compiler';
2
+ import { PACKAGE_NAME } from '../constants';
3
+ import reactivity from '@esportsplus/reactivity/compiler';
4
+ import template from '..';
5
+
6
+
7
+ export default plugin.vite({
8
+ name: PACKAGE_NAME,
9
+ plugins: [reactivity, template]
10
+ });
@@ -0,0 +1,89 @@
1
+ import { ts } from '@esportsplus/typescript';
2
+ import { ENTRYPOINT, ENTRYPOINT_REACTIVITY, TYPES } from './constants';
3
+
4
+
5
+ // Union types that mix functions with non-functions (e.g., Renderable)
6
+ // should fall through to runtime slot dispatch
7
+ function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
8
+ if (type.isUnion()) {
9
+ for (let i = 0, n = type.types.length; i < n; i++) {
10
+ if (!isTypeFunction(type.types[i], checker)) {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ return type.types.length > 0;
16
+ }
17
+
18
+ return type.getCallSignatures().length > 0;
19
+ }
20
+
21
+
22
+ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): TYPES => {
23
+ while (ts.isParenthesizedExpression(expr)) {
24
+ expr = expr.expression;
25
+ }
26
+
27
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
28
+ return TYPES.Effect;
29
+ }
30
+
31
+ // Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
32
+ if (
33
+ ts.isCallExpression(expr) &&
34
+ ts.isPropertyAccessExpression(expr.expression) &&
35
+ ts.isIdentifier(expr.expression.expression) &&
36
+ expr.expression.expression.text === ENTRYPOINT &&
37
+ expr.expression.name.text === ENTRYPOINT_REACTIVITY
38
+ ) {
39
+ return TYPES.ArraySlot;
40
+ }
41
+
42
+ if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === ENTRYPOINT) {
43
+ return TYPES.DocumentFragment;
44
+ }
45
+
46
+ if (
47
+ ts.isNumericLiteral(expr) ||
48
+ ts.isStringLiteral(expr) ||
49
+ ts.isNoSubstitutionTemplateLiteral(expr) ||
50
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
51
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
52
+ expr.kind === ts.SyntaxKind.NullKeyword ||
53
+ expr.kind === ts.SyntaxKind.UndefinedKeyword
54
+ ) {
55
+ return TYPES.Static;
56
+ }
57
+
58
+ if (ts.isTemplateExpression(expr)) {
59
+ return TYPES.Primitive;
60
+ }
61
+
62
+ if (ts.isConditionalExpression(expr)) {
63
+ let whenFalse = analyze(expr.whenFalse, checker),
64
+ whenTrue = analyze(expr.whenTrue, checker);
65
+
66
+ if (whenTrue === whenFalse) {
67
+ return whenTrue;
68
+ }
69
+
70
+ if (whenTrue === TYPES.Effect || whenFalse === TYPES.Effect) {
71
+ return TYPES.Effect;
72
+ }
73
+
74
+ return TYPES.Unknown;
75
+ }
76
+
77
+ if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
78
+ try {
79
+ if (isTypeFunction(checker.getTypeAtLocation(expr), checker)) {
80
+ return TYPES.Effect;
81
+ }
82
+ }
83
+ catch {}
84
+ }
85
+
86
+ return TYPES.Unknown;
87
+ };
88
+
89
+ export { analyze };
@@ -0,0 +1,112 @@
1
+ import { ts } from '@esportsplus/typescript';
2
+ import { imports } from '@esportsplus/typescript/compiler';
3
+ import { ENTRYPOINT, ENTRYPOINT_REACTIVITY, PACKAGE_NAME } from './constants';
4
+
5
+
6
+ type ReactiveCallInfo = {
7
+ arrayArg: ts.Expression;
8
+ callbackArg: ts.Expression;
9
+ end: number;
10
+ node: ts.CallExpression;
11
+ start: number;
12
+ };
13
+
14
+ type TemplateInfo = {
15
+ depth: number;
16
+ end: number;
17
+ expressions: ts.Expression[];
18
+ literals: string[];
19
+ node: ts.TaggedTemplateExpression;
20
+ start: number;
21
+ };
22
+
23
+
24
+ function visitReactiveCalls(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
25
+ if (
26
+ ts.isCallExpression(node) &&
27
+ ts.isPropertyAccessExpression(node.expression) &&
28
+ ts.isIdentifier(node.expression.expression) &&
29
+ node.expression.name.text === ENTRYPOINT_REACTIVITY &&
30
+ node.arguments.length === 2 &&
31
+ node.expression.expression.text === ENTRYPOINT &&
32
+ (!checker || imports.includes(checker, node.expression.expression, PACKAGE_NAME, ENTRYPOINT))
33
+ ) {
34
+ calls.push({
35
+ arrayArg: node.arguments[0],
36
+ callbackArg: node.arguments[1],
37
+ end: node.end,
38
+ node,
39
+ start: node.getStart()
40
+ });
41
+ }
42
+
43
+ ts.forEachChild(node, child => visitReactiveCalls(child, calls, checker));
44
+ }
45
+
46
+ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[], checker: ts.TypeChecker | undefined): void {
47
+ let nextDepth = (ts.isArrowFunction(node) || ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node))
48
+ ? depth + 1
49
+ : depth;
50
+
51
+ if (
52
+ ts.isTaggedTemplateExpression(node) &&
53
+ ts.isIdentifier(node.tag) &&
54
+ node.tag.text === ENTRYPOINT &&
55
+ (!checker || imports.includes(checker, node.tag, PACKAGE_NAME, ENTRYPOINT))
56
+ ) {
57
+ let { expressions, literals } = extractTemplateParts(node.template);
58
+
59
+ templates.push({
60
+ depth,
61
+ end: node.end,
62
+ expressions,
63
+ literals,
64
+ node,
65
+ start: node.getStart()
66
+ });
67
+ }
68
+
69
+ ts.forEachChild(node, child => visitTemplates(child, nextDepth, templates, checker));
70
+ }
71
+
72
+
73
+ const extractTemplateParts = (template: ts.TemplateLiteral): { expressions: ts.Expression[]; literals: string[] } => {
74
+ let expressions: ts.Expression[] = [],
75
+ literals: string[] = [];
76
+
77
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
78
+ literals.push(template.text);
79
+ }
80
+ else if (ts.isTemplateExpression(template)) {
81
+ literals.push(template.head.text);
82
+
83
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
84
+ let span = template.templateSpans[i];
85
+
86
+ expressions.push(span.expression);
87
+ literals.push(span.literal.text);
88
+ }
89
+ }
90
+
91
+ return { expressions, literals };
92
+ };
93
+
94
+ const findHtmlTemplates = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): TemplateInfo[] => {
95
+ let templates: TemplateInfo[] = [];
96
+
97
+ visitTemplates(sourceFile, 0, templates, checker);
98
+
99
+ return templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
100
+ };
101
+
102
+ const findReactiveCalls = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): ReactiveCallInfo[] => {
103
+ let calls: ReactiveCallInfo[] = [];
104
+
105
+ visitReactiveCalls(sourceFile, calls, checker);
106
+
107
+ return calls;
108
+ };
109
+
110
+
111
+ export { extractTemplateParts, findHtmlTemplates, findReactiveCalls };
112
+ export type { ReactiveCallInfo, TemplateInfo };
@@ -0,0 +1,44 @@
1
+ const ARRAY_SLOT = Symbol('template.array.slot');
2
+
3
+ const ATTRIBUTE_DELIMITERS: Record<string, string> = {
4
+ class: ' ',
5
+ style: ';'
6
+ };
7
+
8
+ const CLEANUP = Symbol('template.cleanup');
9
+
10
+ const DIRECT_ATTACH_EVENTS = new Set<string>([
11
+ 'onblur',
12
+ 'onerror',
13
+ 'onfocus', 'onfocusin', 'onfocusout',
14
+ 'onload',
15
+ 'onplay', 'onpause', 'onended', 'ontimeupdate',
16
+ 'onreset',
17
+ 'onscroll', 'onsubmit'
18
+ ]);
19
+
20
+ const LIFECYCLE_EVENTS = new Set<string>([
21
+ 'onconnect', 'ondisconnect', 'onrender', 'onresize', 'ontick'
22
+ ]);
23
+
24
+ const PACKAGE_NAME = '@esportsplus/template';
25
+
26
+ const SLOT_HTML = '<!--$-->';
27
+
28
+ const STATE_HYDRATING = 0;
29
+
30
+ const STATE_NONE = 1;
31
+
32
+ const STATE_WAITING = 2;
33
+
34
+ const STORE = Symbol('template.store');
35
+
36
+
37
+ export {
38
+ ARRAY_SLOT, ATTRIBUTE_DELIMITERS,
39
+ CLEANUP,
40
+ DIRECT_ATTACH_EVENTS,
41
+ LIFECYCLE_EVENTS,
42
+ PACKAGE_NAME,
43
+ SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE,
44
+ };
@@ -0,0 +1,130 @@
1
+ import { root } from '@esportsplus/reactivity';
2
+ import { defineProperty } from '@esportsplus/utilities';
3
+ import { DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants';
4
+ import { ondisconnect as disconnect } from '../slot';
5
+ import { Attributes, Element } from '../types';
6
+ import onconnect from './onconnect';
7
+ import onresize from './onresize';
8
+ import ontick from './ontick';
9
+
10
+
11
+ let controllers = new Map<string, (AbortController & { listeners: number }) | null>(),
12
+ host = window.document,
13
+ keys: Record<string, symbol> = {},
14
+ passive = new Set<string>([
15
+ 'animationend', 'animationiteration', 'animationstart',
16
+ 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousewheel',
17
+ 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover',
18
+ 'scroll',
19
+ 'touchcancel', 'touchend', 'touchleave', 'touchmove', 'touchstart', 'transitionend',
20
+ 'wheel'
21
+ ]);
22
+
23
+
24
+ (['mousemove', 'mousewheel', 'scroll', 'touchend', 'touchmove', 'touchstart', 'wheel'] as string[]).map(event => {
25
+ controllers.set(event, null);
26
+ });
27
+
28
+
29
+ function register(element: Element, event: string) {
30
+ let controller = controllers.get(event),
31
+ signal: AbortController['signal'] | undefined;
32
+
33
+ if (controller === null) {
34
+ let { abort, signal } = new AbortController();
35
+
36
+ controllers.set(
37
+ event,
38
+ controller = {
39
+ abort,
40
+ signal,
41
+ listeners: 0,
42
+ }
43
+ );
44
+ }
45
+
46
+ if (controller) {
47
+ controller.listeners++;
48
+
49
+ ondisconnect(element, () => {
50
+ if (--controller.listeners) {
51
+ return;
52
+ }
53
+
54
+ controller.abort();
55
+ controllers.set(event, null);
56
+ });
57
+ signal = controller.signal;
58
+ }
59
+
60
+ let key = keys[event] = Symbol();
61
+
62
+ host.addEventListener(event, (e) => {
63
+ let fn,
64
+ node = e.target as Element | null;
65
+
66
+ while (node) {
67
+ fn = node[key];
68
+
69
+ if (typeof fn === 'function') {
70
+ defineProperty(e, 'currentTarget', {
71
+ configurable: true,
72
+ get() {
73
+ return node || window.document;
74
+ }
75
+ });
76
+
77
+ return fn.call(node, e);
78
+ }
79
+
80
+ node = node.parentElement as Element | null;
81
+ }
82
+ }, {
83
+ passive: passive.has(event),
84
+ signal
85
+ });
86
+
87
+ return key;
88
+ }
89
+
90
+
91
+ const delegate = <E extends string>(element: Element, event: E, listener: Attributes[`on${E}`]): void => {
92
+ element[ keys[event] || register(element, event) ] = listener;
93
+ };
94
+
95
+ // DIRECT_ATTACH_EVENTS in ./constants.ts tells compiler to use this function
96
+ const on = <E extends string>(element: Element, event: E, listener: Attributes[`on${E}`]): void => {
97
+ let handler = (e: Event) => (listener as Function).call(element, e);
98
+
99
+ element.addEventListener(event, handler, {
100
+ passive: passive.has(event)
101
+ });
102
+
103
+ ondisconnect(element, () => {
104
+ element.removeEventListener(event, handler);
105
+ });
106
+ };
107
+
108
+ const ondisconnect = (element: Element, listener: NonNullable<Attributes[`ondisconnect`]>) => {
109
+ disconnect(element, () => listener(element));
110
+ };
111
+
112
+ const onrender = (element: Element, listener: NonNullable<Attributes[`onrender`]>) => {
113
+ root(() => listener(element));
114
+ };
115
+
116
+ const lifecycle = { onconnect, ondisconnect, onrender, onresize, ontick };
117
+
118
+ const runtime = <E extends `on${string}`>(element: Element, event: E, listener: Attributes[E]): void => {
119
+ let key = event.toLowerCase();
120
+
121
+ if (LIFECYCLE_EVENTS.has(key)) {
122
+ lifecycle[key as keyof typeof lifecycle](element, listener as any);
123
+ }
124
+ else {
125
+ (DIRECT_ATTACH_EVENTS.has(key) ? on : delegate)(element, key.slice(2), listener);
126
+ }
127
+ };
128
+
129
+
130
+ export { delegate, on, onconnect, ondisconnect, onrender, onresize, ontick, runtime };
@@ -0,0 +1,22 @@
1
+ import { root } from '@esportsplus/reactivity';
2
+ import { Attributes, Element } from '../types';
3
+ import { add, remove } from './ontick';
4
+
5
+
6
+ export default (element: Element, listener: NonNullable<Attributes['onconnect']>) => {
7
+ let fn = () => {
8
+ retry--;
9
+
10
+ if (element.isConnected) {
11
+ retry = 0;
12
+ root(() => listener(element));
13
+ }
14
+
15
+ if (!retry) {
16
+ remove(fn);
17
+ }
18
+ },
19
+ retry = 60;
20
+
21
+ add(fn);
22
+ };
@@ -0,0 +1,37 @@
1
+ import { onCleanup } from '@esportsplus/reactivity';
2
+ import { Attributes, Element } from '../types';
3
+
4
+
5
+ let listeners = new Map<Element, Function>(),
6
+ registered = false;
7
+
8
+
9
+ function onresize() {
10
+ for (let [element, fn] of listeners) {
11
+ if (element.isConnected) {
12
+ fn(element);
13
+ }
14
+ else {
15
+ listeners.delete(element);
16
+ }
17
+ }
18
+
19
+ if (listeners.size === 0) {
20
+ window.removeEventListener('resize', onresize);
21
+ registered = false;
22
+ }
23
+ }
24
+
25
+
26
+ export default (element: Element, listener: NonNullable<Attributes['onresize']>) => {
27
+ listeners.set(element, listener);
28
+
29
+ onCleanup(() => {
30
+ listeners.delete(element);
31
+ });
32
+
33
+ if (!registered) {
34
+ window.addEventListener('resize', onresize);
35
+ registered = true;
36
+ }
37
+ };