~repos /atoms-element
git clone https://pyrossh.dev/repos/atoms-element.git
A simple web component library for defining your custom elements. It works on both client and server.
38cee1ba
—
Peter John 4 years ago
add state typings and improve stuff
- example/app-counter.js +37 -58
- index.d.ts +22 -11
- index.js +430 -572
- index.test.js +35 -1
example/app-counter.js
CHANGED
|
@@ -1,64 +1,43 @@
|
|
|
1
|
-
import { createElement,
|
|
1
|
+
import { createElement, createState, html } from '../index.js';
|
|
2
2
|
|
|
3
|
+
export default createElement({
|
|
3
|
-
|
|
4
|
+
name: 'app-counter',
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
attrs: {
|
|
6
|
-
|
|
6
|
+
name: '',
|
|
7
|
-
|
|
7
|
+
meta: {
|
|
8
|
-
|
|
8
|
+
start: '',
|
|
9
|
-
|
|
9
|
+
},
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const stateTypes = () => ({
|
|
13
|
-
count: number()
|
|
14
|
-
.required()
|
|
15
|
-
.default((attrs) => attrs.meta?.start || 0)
|
|
16
|
-
.action('increment', ({ state }) => state.setCount(state.count + 1))
|
|
17
|
-
.action('decrement', ({ state }) => state.setCount(state.count - 1)),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const computedTypes = () => ({
|
|
21
|
-
sum: number()
|
|
22
|
-
.required()
|
|
23
|
-
.compute('count', (count) => {
|
|
24
|
-
return count + 10;
|
|
25
|
-
|
|
10
|
+
},
|
|
26
|
-
|
|
11
|
+
state: {
|
|
27
|
-
|
|
12
|
+
count: 0,
|
|
28
|
-
.compute('count', (count) => {
|
|
29
|
-
return count > 10 ? 'text-red-500' : '';
|
|
30
|
-
|
|
13
|
+
},
|
|
14
|
+
reducer: {
|
|
15
|
+
increment: (state) => ({ ...state, count: state.count + 1 }),
|
|
16
|
+
decrement: (state) => ({ ...state, count: state.count - 1 }),
|
|
31
|
-
}
|
|
17
|
+
},
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
render: ({ attrs, state, actions }) => {
|
|
34
|
-
|
|
19
|
+
const { name, meta } = attrs;
|
|
35
|
-
|
|
20
|
+
const { count } = state;
|
|
36
|
-
|
|
21
|
+
const sum = count + 10;
|
|
22
|
+
const warningClass = count > 10 ? 'text-red-500' : '';
|
|
37
23
|
|
|
38
|
-
|
|
24
|
+
return html`
|
|
39
|
-
|
|
25
|
+
<div>
|
|
40
|
-
|
|
26
|
+
<div class="mb-2">
|
|
41
|
-
|
|
27
|
+
Counter: ${name}
|
|
42
|
-
|
|
28
|
+
<span>starts at ${meta?.start}</span>
|
|
43
|
-
|
|
29
|
+
</div>
|
|
44
|
-
|
|
30
|
+
<div class="flex flex-1 flex-row items-center text-gray-700">
|
|
45
|
-
|
|
31
|
+
<button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${actions.decrement}>-</button>
|
|
32
|
+
<div class="mx-20">
|
|
33
|
+
<h1 class="text-3xl font-mono ${warningClass}">${count}</h1>
|
|
34
|
+
</div>
|
|
35
|
+
<button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${actions.increment}>+</button>
|
|
36
|
+
</div>
|
|
46
37
|
<div class="mx-20">
|
|
47
|
-
<h1 class="text-
|
|
38
|
+
<h1 class="text-xl font-mono">Sum: ${sum}</h1>
|
|
48
39
|
</div>
|
|
49
|
-
<button class="bg-gray-300 text-gray-700 rounded hover:bg-gray-200 px-4 py-2 text-3xl focus:outline-none" @click=${increment}>+</button>
|
|
50
40
|
</div>
|
|
51
|
-
<div class="mx-20">
|
|
52
|
-
<h1 class="text-xl font-mono">Sum: ${sum}</h1>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
41
|
+
`;
|
|
56
|
-
}
|
|
42
|
+
},
|
|
57
|
-
|
|
58
|
-
export default createElement({
|
|
59
|
-
name,
|
|
60
|
-
attrTypes,
|
|
61
|
-
stateTypes,
|
|
62
|
-
computedTypes,
|
|
63
|
-
render,
|
|
64
43
|
});
|
index.d.ts
CHANGED
|
@@ -44,16 +44,6 @@ export class AtomsElement {
|
|
|
44
44
|
export const getConfig = () => Config;
|
|
45
45
|
export const getLocation = () => Location;
|
|
46
46
|
|
|
47
|
-
export type CreateElementProps = {
|
|
48
|
-
name: () => string;
|
|
49
|
-
attrTypes?: () => {[key: string]: any};
|
|
50
|
-
stateTypes?: () => {[key: string]: any};
|
|
51
|
-
computedTypes?: () => {[key: string]: any};
|
|
52
|
-
render: () => any
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const createElement = (props: CreateElementProps) => CreateElementProps;
|
|
56
|
-
|
|
57
47
|
export type PageRenderProps = {
|
|
58
48
|
props: any;
|
|
59
49
|
headScript: string;
|
|
@@ -61,4 +51,25 @@ export type PageRenderProps = {
|
|
|
61
51
|
}
|
|
62
52
|
export type Handler = (props: any) => string;
|
|
63
53
|
|
|
64
|
-
export const createPage = (props: { head: Handler, body: Handler}) => (props: PageRenderProps) => string;
|
|
54
|
+
export const createPage = (props: { head: Handler, body: Handler}) => (props: PageRenderProps) => string;
|
|
55
|
+
|
|
56
|
+
export type State<P, Q> = {
|
|
57
|
+
getState: () => P;
|
|
58
|
+
subscribe: (fn: (v: P) => void) => void;
|
|
59
|
+
} & { [K in keyof Q]: (v: any) => void}
|
|
60
|
+
|
|
61
|
+
export function createState<P, Q extends {[k: string]: (state: P, v: any) => P}>(props: { state: P, reducer: Q }): State<P, Q>;
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export type ElementState<P, Q> = P & { [K in keyof Q]: (v: any) => void}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export type CreateElementProps<N, P, Q> = {
|
|
68
|
+
name: string;
|
|
69
|
+
attrs: N;
|
|
70
|
+
state: State<P, Q>;
|
|
71
|
+
reducer: Q
|
|
72
|
+
render: (props: { attrs: N, state: P, actions: { [K in keyof Q]: (v: any) => void;} }) => any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createElement<N, P, Q extends {[k: string]: (state: P, v: any) => P}>(props: CreateElementProps<N, P, Q>): CreateElementProps<N, P, Q>;
|
index.js
CHANGED
|
@@ -3,6 +3,280 @@ import { html, render as litRender, directive, NodePart, isPrimitive } from './l
|
|
|
3
3
|
const isBrowser = typeof window !== 'undefined';
|
|
4
4
|
export { html, isBrowser };
|
|
5
5
|
|
|
6
|
+
const lastAttributeNameRegex =
|
|
7
|
+
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
8
|
+
const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
|
|
9
|
+
const whitespaceRE = /^\s*$/;
|
|
10
|
+
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
|
|
11
|
+
const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
12
|
+
|
|
13
|
+
const parseTag = (tag) => {
|
|
14
|
+
const res = {
|
|
15
|
+
type: 'tag',
|
|
16
|
+
name: '',
|
|
17
|
+
voidElement: false,
|
|
18
|
+
attrs: {},
|
|
19
|
+
children: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
|
|
23
|
+
if (tagMatch) {
|
|
24
|
+
res.name = tagMatch[1];
|
|
25
|
+
if (voidElements.includes(tagMatch[1]) || tag.charAt(tag.length - 2) === '/') {
|
|
26
|
+
res.voidElement = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// handle comment tag
|
|
30
|
+
if (res.name.startsWith('!--')) {
|
|
31
|
+
const endIndex = tag.indexOf('-->');
|
|
32
|
+
return {
|
|
33
|
+
type: 'comment',
|
|
34
|
+
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const reg = new RegExp(attrRE);
|
|
40
|
+
let result = null;
|
|
41
|
+
for (;;) {
|
|
42
|
+
result = reg.exec(tag);
|
|
43
|
+
|
|
44
|
+
if (result === null) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!result[0].trim()) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result[1]) {
|
|
53
|
+
const attr = result[1].trim();
|
|
54
|
+
let arr = [attr, ''];
|
|
55
|
+
|
|
56
|
+
if (attr.indexOf('=') > -1) {
|
|
57
|
+
arr = attr.split('=');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.attrs[arr[0]] = arr[1];
|
|
61
|
+
reg.lastIndex--;
|
|
62
|
+
} else if (result[2]) {
|
|
63
|
+
res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return res;
|
|
68
|
+
};
|
|
69
|
+
const parseHtml = (html) => {
|
|
70
|
+
const result = [];
|
|
71
|
+
const arr = [];
|
|
72
|
+
let current;
|
|
73
|
+
let level = -1;
|
|
74
|
+
|
|
75
|
+
// handle text at top level
|
|
76
|
+
if (html.indexOf('<') !== 0) {
|
|
77
|
+
var end = html.indexOf('<');
|
|
78
|
+
result.push({
|
|
79
|
+
type: 'text',
|
|
80
|
+
content: end === -1 ? html : html.substring(0, end),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
html.replace(tagRE, function (tag, index) {
|
|
85
|
+
const isOpen = tag.charAt(1) !== '/';
|
|
86
|
+
const isComment = tag.startsWith('<!--');
|
|
87
|
+
const start = index + tag.length;
|
|
88
|
+
const nextChar = html.charAt(start);
|
|
89
|
+
let parent;
|
|
90
|
+
|
|
91
|
+
if (isComment) {
|
|
92
|
+
const comment = parseTag(tag);
|
|
93
|
+
|
|
94
|
+
// if we're at root, push new base node
|
|
95
|
+
if (level < 0) {
|
|
96
|
+
result.push(comment);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
parent = arr[level];
|
|
100
|
+
parent.children.push(comment);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isOpen) {
|
|
105
|
+
level++;
|
|
106
|
+
|
|
107
|
+
current = parseTag(tag);
|
|
108
|
+
|
|
109
|
+
if (!current.voidElement && nextChar && nextChar !== '<') {
|
|
110
|
+
current.children.push({
|
|
111
|
+
type: 'text',
|
|
112
|
+
content: html.slice(start, html.indexOf('<', start)),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// if we're at root, push new base node
|
|
117
|
+
if (level === 0) {
|
|
118
|
+
result.push(current);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
parent = arr[level - 1];
|
|
122
|
+
|
|
123
|
+
if (parent) {
|
|
124
|
+
parent.children.push(current);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
arr[level] = current;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isOpen || current.voidElement) {
|
|
131
|
+
if (level > -1 && (current.voidElement || current.name === tag.slice(2, -1))) {
|
|
132
|
+
level--;
|
|
133
|
+
// move current up a level to match the end tag
|
|
134
|
+
current = level === -1 ? result : arr[level];
|
|
135
|
+
}
|
|
136
|
+
if (nextChar !== '<' && nextChar) {
|
|
137
|
+
// trailing text node
|
|
138
|
+
// if we're at the root, push a base text node. otherwise add as
|
|
139
|
+
// a child to the current node.
|
|
140
|
+
parent = level === -1 ? result : arr[level].children;
|
|
141
|
+
|
|
142
|
+
// calculate correct end of the content slice in case there's
|
|
143
|
+
// no tag after the text node.
|
|
144
|
+
const end = html.indexOf('<', start);
|
|
145
|
+
let content = html.slice(start, end === -1 ? undefined : end);
|
|
146
|
+
// if a node is nothing but whitespace, collapse it as the spec states:
|
|
147
|
+
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
|
|
148
|
+
if (whitespaceRE.test(content)) {
|
|
149
|
+
content = ' ';
|
|
150
|
+
}
|
|
151
|
+
// don't add whitespace-only text nodes if they would be trailing text nodes
|
|
152
|
+
// or if they would be leading whitespace-only text nodes:
|
|
153
|
+
// * end > -1 indicates this is not a trailing text node
|
|
154
|
+
// * leading node is when level is -1 and parent has length 0
|
|
155
|
+
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
|
|
156
|
+
parent.push({
|
|
157
|
+
type: 'text',
|
|
158
|
+
content: content,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const stringifyAttrs = (attrs) => {
|
|
169
|
+
const buff = [];
|
|
170
|
+
for (let key in attrs) {
|
|
171
|
+
buff.push(key + '="' + attrs[key] + '"');
|
|
172
|
+
}
|
|
173
|
+
if (!buff.length) {
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
return ' ' + buff.join(' ');
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const stringifyHtml = (buff, doc) => {
|
|
180
|
+
switch (doc.type) {
|
|
181
|
+
case 'text':
|
|
182
|
+
return buff + doc.content;
|
|
183
|
+
case 'tag':
|
|
184
|
+
buff += '<' + doc.name + (doc.attrs ? stringifyAttrs(doc.attrs) : '') + (doc.voidElement ? '/>' : '>');
|
|
185
|
+
if (doc.voidElement) {
|
|
186
|
+
return buff;
|
|
187
|
+
}
|
|
188
|
+
return buff + doc.children.reduce(stringifyHtml, '') + '</' + doc.name + '>';
|
|
189
|
+
case 'comment':
|
|
190
|
+
buff += '<!--' + doc.comment + '-->';
|
|
191
|
+
return buff;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const hydrate = (node) => {
|
|
196
|
+
const Clazz = getElement(node.name);
|
|
197
|
+
if (Clazz) {
|
|
198
|
+
const newAttrs = {};
|
|
199
|
+
Object.keys(node.attrs).forEach((key) => {
|
|
200
|
+
const newValue = node.attrs[key];
|
|
201
|
+
newAttrs[key] = newValue && newValue.startsWith(`{`) ? JSON.parse(newValue.replace(/'/g, `"`)) : newValue;
|
|
202
|
+
});
|
|
203
|
+
const instance = new Clazz(newAttrs);
|
|
204
|
+
const res = instance.render();
|
|
205
|
+
node.children = parseHtml(res);
|
|
206
|
+
}
|
|
207
|
+
if (node.children) {
|
|
208
|
+
for (const child of node.children) {
|
|
209
|
+
hydrate(child);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const wrapAttribute = (attrName, suffix, text, v) => {
|
|
215
|
+
let buffer = text;
|
|
216
|
+
const hasQuote = suffix && suffix.includes(`="`);
|
|
217
|
+
if (attrName && !hasQuote) {
|
|
218
|
+
buffer += `"`;
|
|
219
|
+
}
|
|
220
|
+
buffer += v;
|
|
221
|
+
if (attrName && !hasQuote) {
|
|
222
|
+
buffer += `"`;
|
|
223
|
+
}
|
|
224
|
+
return buffer;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const renderHtml = isBrowser
|
|
228
|
+
? litRender
|
|
229
|
+
: (template) => {
|
|
230
|
+
let js = '';
|
|
231
|
+
template.strings.forEach((text, i) => {
|
|
232
|
+
const value = template.values[i];
|
|
233
|
+
const type = typeof value;
|
|
234
|
+
let attrName, suffix;
|
|
235
|
+
const matchName = lastAttributeNameRegex.exec(text);
|
|
236
|
+
if (matchName) {
|
|
237
|
+
attrName = matchName[2];
|
|
238
|
+
suffix = matchName[3];
|
|
239
|
+
}
|
|
240
|
+
if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
|
|
241
|
+
js += wrapAttribute(attrName, suffix, text, type !== 'string' ? String(value) : value);
|
|
242
|
+
} else if (Array.isArray(value) && value.find((item) => item && item.strings && item.type === 'html')) {
|
|
243
|
+
js += text;
|
|
244
|
+
value.forEach((v) => {
|
|
245
|
+
js += renderHtml(v);
|
|
246
|
+
});
|
|
247
|
+
} else if (type === 'object') {
|
|
248
|
+
// TemplateResult
|
|
249
|
+
if (value.strings && value.type === 'html') {
|
|
250
|
+
js += text;
|
|
251
|
+
js += renderHtml(value);
|
|
252
|
+
} else {
|
|
253
|
+
js += wrapAttribute(attrName, suffix, text, JSON.stringify(value).replace(/"/g, `'`));
|
|
254
|
+
}
|
|
255
|
+
} else if (type == 'function') {
|
|
256
|
+
if (attrName) {
|
|
257
|
+
js += text.replace(' ' + attrName + '=', '');
|
|
258
|
+
} else {
|
|
259
|
+
// js += text;
|
|
260
|
+
// js += value();
|
|
261
|
+
}
|
|
262
|
+
} else if (type !== 'undefined') {
|
|
263
|
+
js += text;
|
|
264
|
+
js += value.toString();
|
|
265
|
+
} else {
|
|
266
|
+
js += text;
|
|
267
|
+
// console.log('value', value);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
const nodes = parseHtml(js);
|
|
271
|
+
for (const node of nodes) {
|
|
272
|
+
hydrate(node);
|
|
273
|
+
}
|
|
274
|
+
const html = nodes.reduce((acc, node) => {
|
|
275
|
+
return acc + stringifyHtml('', node);
|
|
276
|
+
}, '');
|
|
277
|
+
return html;
|
|
278
|
+
};
|
|
279
|
+
|
|
6
280
|
const hyphenate = (s) => s.replace(/[A-Z]|^ms/g, '-$&').toLowerCase();
|
|
7
281
|
const percent = (v) => (v * 100).toFixed(2) + '%';
|
|
8
282
|
const createStyle = (...kvs) => {
|
|
@@ -57,7 +331,8 @@ const extractClasses = (html) => {
|
|
|
57
331
|
return str;
|
|
58
332
|
};
|
|
59
333
|
|
|
60
|
-
|
|
334
|
+
const getClassList = (template) => {
|
|
335
|
+
// need to have this incase of shadowDom
|
|
61
336
|
// const classes = template.strings.reduce((acc, item) => acc + extractClasses(item), '').split(' ');
|
|
62
337
|
const classes = [];
|
|
63
338
|
template.values.forEach((item) => {
|
|
@@ -70,19 +345,6 @@ export const getClassList = (template) => {
|
|
|
70
345
|
return classes;
|
|
71
346
|
};
|
|
72
347
|
|
|
73
|
-
export const apply = (classes) => {
|
|
74
|
-
const classStyles = {};
|
|
75
|
-
classes.split(' ').forEach((cls) => {
|
|
76
|
-
const styles = classLookup[cls];
|
|
77
|
-
if (styles) {
|
|
78
|
-
Object.keys(styles).forEach((key) => {
|
|
79
|
-
classStyles[key] = styles[key];
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
return classStyles;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
348
|
export const generateTWStyleSheet = (classList) => {
|
|
87
349
|
let styleSheet = ``;
|
|
88
350
|
classList.forEach((cls) => {
|
|
@@ -630,593 +892,190 @@ const pageStyles = {
|
|
|
630
892
|
display: 'flex',
|
|
631
893
|
flexDirection: 'column',
|
|
632
894
|
flex: '1 1 0%',
|
|
633
|
-
minWidth: '320px',
|
|
634
|
-
minHeight: '100vh',
|
|
635
|
-
fontWeight: 400,
|
|
636
|
-
color: 'rgba(44, 62, 80, 1)',
|
|
637
|
-
direction: 'ltr',
|
|
638
|
-
fontSynthesis: 'none',
|
|
639
|
-
textRendering: 'optimizeLegibility',
|
|
640
|
-
},
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
// hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500
|
|
644
|
-
|
|
645
|
-
const previousValues = new WeakMap();
|
|
646
|
-
export const unsafeHTML = isBrowser
|
|
647
|
-
? directive((value) => (part) => {
|
|
648
|
-
if (!(part instanceof NodePart)) {
|
|
649
|
-
throw new Error('unsafeHTML can only be used in text bindings');
|
|
650
|
-
}
|
|
651
|
-
const previousValue = previousValues.get(part);
|
|
652
|
-
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
const template = document.createElement('template');
|
|
656
|
-
template.innerHTML = value; // innerHTML casts to string internally
|
|
657
|
-
const fragment = document.importNode(template.content, true);
|
|
658
|
-
part.setValue(fragment);
|
|
659
|
-
previousValues.set(part, { value, fragment });
|
|
660
|
-
})
|
|
661
|
-
: (value) => value;
|
|
662
|
-
|
|
663
|
-
const logError = (msg) => {
|
|
664
|
-
if (isBrowser ? window.__DEV__ : global.__DEV) {
|
|
665
|
-
console.warn(msg);
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const validator = (type, validate) => (innerType) => {
|
|
670
|
-
const isPrimitiveType = ['number', 'string', 'boolean'].includes(type);
|
|
671
|
-
const common = {
|
|
672
|
-
type: type,
|
|
673
|
-
parse: isPrimitiveType ? (attr) => attr : (attr) => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
|
|
674
|
-
validate: (context, data) => {
|
|
675
|
-
if (data === null || typeof data === 'undefined') {
|
|
676
|
-
if (common.__required) {
|
|
677
|
-
logError(`'${context}' Field is required`);
|
|
678
|
-
}
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
if (!isPrimitiveType) {
|
|
682
|
-
validate(innerType, context, data);
|
|
683
|
-
} else {
|
|
684
|
-
const dataType = typeof data;
|
|
685
|
-
if (dataType !== type) {
|
|
686
|
-
logError(`'${context}' Expected type '${type}' got type '${dataType}'`);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
},
|
|
690
|
-
};
|
|
691
|
-
common.required = () => {
|
|
692
|
-
common.__required = true;
|
|
693
|
-
return common;
|
|
694
|
-
};
|
|
695
|
-
common.default = (fnOrValue) => {
|
|
696
|
-
common.__default = fnOrValue;
|
|
697
|
-
return common;
|
|
698
|
-
};
|
|
699
|
-
common.compute = (...args) => {
|
|
700
|
-
const fn = args[args.length - 1];
|
|
701
|
-
const deps = args.slice(0, args.length - 1);
|
|
702
|
-
common.__compute = {
|
|
703
|
-
fn,
|
|
704
|
-
deps,
|
|
705
|
-
};
|
|
706
|
-
return common;
|
|
707
|
-
};
|
|
708
|
-
common.action = (name, fn) => {
|
|
709
|
-
if (!common.__handlers) {
|
|
710
|
-
common.__handlers = {};
|
|
711
|
-
}
|
|
712
|
-
common.__handlers[name] = fn;
|
|
713
|
-
return common;
|
|
714
|
-
};
|
|
715
|
-
return common;
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
export const number = validator('number');
|
|
719
|
-
export const string = validator('string');
|
|
720
|
-
export const boolean = validator('boolean');
|
|
721
|
-
export const object = validator('object', (innerType, context, data) => {
|
|
722
|
-
if (data.constructor !== Object) {
|
|
723
|
-
logError(`'${context}' Expected object literal '{}' got '${typeof data}'`);
|
|
724
|
-
}
|
|
725
|
-
for (const key of Object.keys(innerType)) {
|
|
726
|
-
const fieldValidator = innerType[key];
|
|
727
|
-
const item = data[key];
|
|
728
|
-
fieldValidator.validate(`${context}.${key}`, item);
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
export const array = validator('array', (innerType, context, data) => {
|
|
732
|
-
if (!Array.isArray(data)) {
|
|
733
|
-
logError(`Expected Array got ${data}`);
|
|
734
|
-
}
|
|
735
|
-
for (let i = 0; i < data.length; i++) {
|
|
736
|
-
const item = data[i];
|
|
737
|
-
innerType.validate(`${context}[${i}]`, item);
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
const fifo = (q) => q.shift();
|
|
742
|
-
const microtask = (flush) => () => queueMicrotask(flush);
|
|
743
|
-
|
|
744
|
-
const registry = {};
|
|
745
|
-
const BaseElement = isBrowser ? window.HTMLElement : class {};
|
|
746
|
-
|
|
747
|
-
export class AtomsElement extends BaseElement {
|
|
748
|
-
static register() {
|
|
749
|
-
registry[this.name] = this;
|
|
750
|
-
if (isBrowser) {
|
|
751
|
-
if (window.customElements.get(this.name)) {
|
|
752
|
-
return;
|
|
753
|
-
} else {
|
|
754
|
-
window.customElements.define(this.name, registry[this.name]);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
static getElement(name) {
|
|
760
|
-
return registry[name];
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
static get observedAttributes() {
|
|
764
|
-
if (!this.attrTypes) {
|
|
765
|
-
return [];
|
|
766
|
-
}
|
|
767
|
-
return Object.keys(this.attrTypes).map((k) => k.toLowerCase());
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
constructor(attrs) {
|
|
771
|
-
super();
|
|
772
|
-
this._dirty = false;
|
|
773
|
-
this._connected = false;
|
|
774
|
-
this.attrs = attrs || {};
|
|
775
|
-
this.state = {};
|
|
776
|
-
this.config = isBrowser ? window.config : global.config;
|
|
777
|
-
this.location = isBrowser ? window.location : global.location;
|
|
778
|
-
this.prevClassList = [];
|
|
779
|
-
if (!isBrowser) {
|
|
780
|
-
this.initState();
|
|
781
|
-
} else {
|
|
782
|
-
// this.shadow = this.attachShadow({ mode: 'open' });
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
initAttrs() {
|
|
787
|
-
Object.keys(this.constructor.attrTypes).forEach((key) => {
|
|
788
|
-
const attrType = this.constructor.attrTypes[key];
|
|
789
|
-
const newValue = this.getAttribute(key.toLowerCase());
|
|
790
|
-
const data = attrType.parse(newValue);
|
|
791
|
-
attrType.validate(`<${this.constructor.name}> ${key}`, data);
|
|
792
|
-
this.attrs[key] = data;
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
initState() {
|
|
797
|
-
Object.keys(this.constructor.stateTypes).forEach((key) => {
|
|
798
|
-
const stateType = this.constructor.stateTypes[key];
|
|
799
|
-
if (!this.state[key] && typeof stateType.__default !== 'undefined') {
|
|
800
|
-
this.state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this.state) : stateType.__default;
|
|
801
|
-
}
|
|
802
|
-
const setKey = `set${key[0].toUpperCase()}${key.slice(1)}`;
|
|
803
|
-
this.state[setKey] = (v) => {
|
|
804
|
-
// TODO: check type on set
|
|
805
|
-
this.state[key] = typeof v === 'function' ? v(this.state[key]) : v;
|
|
806
|
-
this.update();
|
|
807
|
-
};
|
|
808
|
-
if (stateType.__handlers) {
|
|
809
|
-
Object.keys(stateType.__handlers).map((hkey) => {
|
|
810
|
-
this.state[hkey] = () => stateType.__handlers[hkey]({ attrs: this.attrs, state: this.state });
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
connectedCallback() {
|
|
817
|
-
this._connected = true;
|
|
818
|
-
this.initAttrs();
|
|
819
|
-
this.initState();
|
|
820
|
-
this.update();
|
|
821
|
-
}
|
|
822
|
-
disconnectedCallback() {
|
|
823
|
-
this._connected = false;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
attributeChangedCallback(key, oldValue, newValue) {
|
|
827
|
-
if (this._connected) {
|
|
828
|
-
this.initAttrs();
|
|
829
|
-
this.update();
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
update() {
|
|
834
|
-
if (this._dirty) {
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
this._dirty = true;
|
|
838
|
-
this.enqueueUpdate();
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
_performUpdate() {
|
|
842
|
-
if (!this._connected) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
this.renderTemplate();
|
|
846
|
-
this._dirty = false;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
batch(runner, pick, callback) {
|
|
850
|
-
const q = [];
|
|
851
|
-
const flush = () => {
|
|
852
|
-
let p;
|
|
853
|
-
while ((p = pick(q))) callback(p);
|
|
854
|
-
};
|
|
855
|
-
const run = runner(flush);
|
|
856
|
-
q.push(this) === 1 && run();
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
enqueueUpdate() {
|
|
860
|
-
this.batch(microtask, fifo, () => this._performUpdate());
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
get computed() {
|
|
864
|
-
return Object.keys(this.constructor.computedTypes).reduceRight((acc, key) => {
|
|
865
|
-
const type = this.constructor.computedTypes[key];
|
|
866
|
-
const state = this.state;
|
|
867
|
-
const values = type.__compute.deps.reduce((dacc, key) => {
|
|
868
|
-
if (typeof state[key] !== undefined) {
|
|
869
|
-
dacc.push(state[key]);
|
|
870
|
-
}
|
|
871
|
-
return dacc;
|
|
872
|
-
}, []);
|
|
873
|
-
acc[key] = type.__compute.fn(...values);
|
|
874
|
-
return acc;
|
|
875
|
-
}, {});
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
renderTemplate() {
|
|
879
|
-
const template = this.render();
|
|
880
|
-
if (isBrowser) {
|
|
881
|
-
// TODO: this can be optimized when we know whether the value belongs in a class (AttributePart)
|
|
882
|
-
// maybe do this in lit-html itselfs
|
|
883
|
-
const newClassList = getClassList(template).filter((cls) => {
|
|
884
|
-
const globalStyles = document.getElementById('global').textContent;
|
|
885
|
-
return !globalStyles.includes('.' + cls);
|
|
886
|
-
});
|
|
887
|
-
if (newClassList.length > 0) {
|
|
888
|
-
document.getElementById('global').textContent += generateTWStyleSheet(newClassList);
|
|
889
|
-
}
|
|
890
|
-
render(template, this);
|
|
891
|
-
// For shadows only
|
|
892
|
-
// if (!this.styleElement) {
|
|
893
|
-
// render(template, this.shadow);
|
|
894
|
-
// const styleSheet = generateTWStyleSheet(classList);
|
|
895
|
-
// this.prevClassList = classList;
|
|
896
|
-
// this.styleElement = document.createElement('style');
|
|
897
|
-
// this.shadow.appendChild(this.styleElement).textContent = css(pageStyles) + styleSheet;
|
|
898
|
-
// } else {
|
|
899
|
-
// const missingClassList = classList.filter((cls) => !this.prevClassList.includes(cls));
|
|
900
|
-
// if (missingClassList.length > 0) {
|
|
901
|
-
// const styleSheet = generateTWStyleSheet(missingClassList);
|
|
902
|
-
// this.styleElement.textContent += '\n' + styleSheet;
|
|
903
|
-
// this.prevClassList.push(...missingClassList);
|
|
904
|
-
// }
|
|
905
|
-
// render(template, this.shadow);
|
|
906
|
-
// }
|
|
907
|
-
} else {
|
|
908
|
-
return render(template);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
export const getConfig = () => (isBrowser ? window.props.config : global.props.config);
|
|
913
|
-
export const getLocation = () => (isBrowser ? window.location : global.location);
|
|
914
|
-
|
|
915
|
-
export const createElement = ({ name, attrTypes, stateTypes, computedTypes, render }) => {
|
|
916
|
-
const Element = class extends AtomsElement {
|
|
917
|
-
static name = name();
|
|
918
|
-
|
|
919
|
-
static attrTypes = attrTypes ? attrTypes() : {};
|
|
920
|
-
|
|
921
|
-
static stateTypes = stateTypes ? stateTypes() : {};
|
|
922
|
-
|
|
923
|
-
static computedTypes = computedTypes ? computedTypes() : {};
|
|
924
|
-
|
|
925
|
-
render() {
|
|
926
|
-
return render({
|
|
927
|
-
attrs: this.attrs,
|
|
928
|
-
state: this.state,
|
|
929
|
-
computed: this.computed,
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
};
|
|
933
|
-
Element.register();
|
|
934
|
-
return { name, attrTypes, stateTypes, computedTypes, render };
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
const lastAttributeNameRegex =
|
|
938
|
-
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
939
|
-
const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
|
|
940
|
-
const whitespaceRE = /^\s*$/;
|
|
941
|
-
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
|
|
942
|
-
const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
943
|
-
|
|
944
|
-
const parseTag = (tag) => {
|
|
945
|
-
const res = {
|
|
946
|
-
type: 'tag',
|
|
947
|
-
name: '',
|
|
948
|
-
voidElement: false,
|
|
949
|
-
attrs: {},
|
|
950
|
-
children: [],
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
|
|
954
|
-
if (tagMatch) {
|
|
955
|
-
res.name = tagMatch[1];
|
|
956
|
-
if (voidElements.includes(tagMatch[1]) || tag.charAt(tag.length - 2) === '/') {
|
|
957
|
-
res.voidElement = true;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// handle comment tag
|
|
961
|
-
if (res.name.startsWith('!--')) {
|
|
962
|
-
const endIndex = tag.indexOf('-->');
|
|
963
|
-
return {
|
|
964
|
-
type: 'comment',
|
|
965
|
-
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const reg = new RegExp(attrRE);
|
|
971
|
-
let result = null;
|
|
972
|
-
for (;;) {
|
|
973
|
-
result = reg.exec(tag);
|
|
974
|
-
|
|
975
|
-
if (result === null) {
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
if (!result[0].trim()) {
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
895
|
+
minWidth: '320px',
|
|
896
|
+
minHeight: '100vh',
|
|
897
|
+
fontWeight: 400,
|
|
898
|
+
color: 'rgba(44, 62, 80, 1)',
|
|
899
|
+
direction: 'ltr',
|
|
900
|
+
fontSynthesis: 'none',
|
|
901
|
+
textRendering: 'optimizeLegibility',
|
|
902
|
+
},
|
|
903
|
+
};
|
|
982
904
|
|
|
983
|
-
if (result[1]) {
|
|
984
|
-
|
|
905
|
+
// hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500
|
|
985
|
-
let arr = [attr, ''];
|
|
986
906
|
|
|
907
|
+
const previousValues = new WeakMap();
|
|
908
|
+
export const unsafeHTML = isBrowser
|
|
987
|
-
|
|
909
|
+
? directive((value) => (part) => {
|
|
988
|
-
|
|
910
|
+
if (!(part instanceof NodePart)) {
|
|
911
|
+
throw new Error('unsafeHTML can only be used in text bindings');
|
|
912
|
+
}
|
|
913
|
+
const previousValue = previousValues.get(part);
|
|
914
|
+
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
|
915
|
+
return;
|
|
989
916
|
}
|
|
917
|
+
const template = document.createElement('template');
|
|
918
|
+
template.innerHTML = value; // innerHTML casts to string internally
|
|
919
|
+
const fragment = document.importNode(template.content, true);
|
|
920
|
+
part.setValue(fragment);
|
|
921
|
+
previousValues.set(part, { value, fragment });
|
|
922
|
+
})
|
|
923
|
+
: (value) => value;
|
|
990
924
|
|
|
991
|
-
res.attrs[arr[0]] = arr[1];
|
|
992
|
-
reg.lastIndex--;
|
|
993
|
-
|
|
925
|
+
const fifo = (q) => q.shift();
|
|
994
|
-
|
|
926
|
+
const microtask = (flush) => () => queueMicrotask(flush);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
927
|
|
|
928
|
+
export const createState = ({ state, reducer }) => {
|
|
929
|
+
let initial = state;
|
|
930
|
+
const subs = new Set();
|
|
931
|
+
return {
|
|
932
|
+
getValue: () => initial,
|
|
933
|
+
subscribe: (fn) => {
|
|
934
|
+
subs.add(fn);
|
|
935
|
+
},
|
|
936
|
+
unsubscribe: (fn) => {
|
|
937
|
+
subs.remove(fn);
|
|
938
|
+
},
|
|
939
|
+
actions: Object.keys(reducer).reduce((acc, key) => {
|
|
940
|
+
const reduce = reducer[key];
|
|
941
|
+
acc[key] = (v) => {
|
|
942
|
+
initial = reduce(initial, v);
|
|
943
|
+
subs.forEach((sub) => {
|
|
944
|
+
sub(initial);
|
|
945
|
+
});
|
|
946
|
+
};
|
|
998
|
-
|
|
947
|
+
return acc;
|
|
948
|
+
}, {}),
|
|
949
|
+
};
|
|
999
950
|
};
|
|
1000
|
-
const parseHtml = (html) => {
|
|
1001
|
-
const result = [];
|
|
1002
|
-
const arr = [];
|
|
1003
|
-
let current;
|
|
1004
|
-
let level = -1;
|
|
1005
951
|
|
|
952
|
+
const registry = {};
|
|
953
|
+
export const getConfig = () => (isBrowser ? window.props.config : global.props.config);
|
|
954
|
+
export const getLocation = () => (isBrowser ? window.location : global.location);
|
|
1006
|
-
|
|
955
|
+
export const getElement = (name) => registry[name];
|
|
956
|
+
export const registerElement = (name, clazz) => {
|
|
957
|
+
registry[name] = clazz;
|
|
958
|
+
if (isBrowser) {
|
|
1007
|
-
|
|
959
|
+
if (window.customElements.get(name)) {
|
|
1008
|
-
var end = html.indexOf('<');
|
|
1009
|
-
|
|
960
|
+
return;
|
|
1010
|
-
|
|
961
|
+
} else {
|
|
1011
|
-
|
|
962
|
+
window.customElements.define(name, registry[name]);
|
|
1012
|
-
}
|
|
963
|
+
}
|
|
1013
964
|
}
|
|
1014
|
-
|
|
1015
|
-
html.replace(tagRE, function (tag, index) {
|
|
1016
|
-
const isOpen = tag.charAt(1) !== '/';
|
|
1017
|
-
const isComment = tag.startsWith('<!--');
|
|
1018
|
-
const start = index + tag.length;
|
|
1019
|
-
const nextChar = html.charAt(start);
|
|
1020
|
-
let parent;
|
|
1021
|
-
|
|
1022
|
-
if (isComment) {
|
|
1023
|
-
const comment = parseTag(tag);
|
|
1024
|
-
|
|
1025
|
-
// if we're at root, push new base node
|
|
1026
|
-
if (level < 0) {
|
|
1027
|
-
result.push(comment);
|
|
1028
|
-
return result;
|
|
1029
|
-
|
|
965
|
+
};
|
|
1030
|
-
|
|
966
|
+
const BaseElement = isBrowser ? window.HTMLElement : class {};
|
|
967
|
+
export const createElement = ({ name, attrs, state, reducer, render: renderFn }) => {
|
|
1031
|
-
|
|
968
|
+
const Element = class extends BaseElement {
|
|
1032
|
-
|
|
969
|
+
static get observedAttributes() {
|
|
970
|
+
return Object.keys(attrs || {}).map((k) => k.toLowerCase());
|
|
1033
971
|
}
|
|
1034
972
|
|
|
1035
|
-
|
|
973
|
+
constructor(ssrAttrs) {
|
|
1036
|
-
|
|
974
|
+
super();
|
|
1037
|
-
|
|
975
|
+
this._dirty = false;
|
|
976
|
+
this._connected = false;
|
|
1038
|
-
|
|
977
|
+
this.attrs = ssrAttrs || attrs;
|
|
1039
|
-
|
|
978
|
+
this.state = createState({ state, reducer });
|
|
979
|
+
this.config = isBrowser ? window.config : global.config;
|
|
1040
|
-
|
|
980
|
+
this.location = isBrowser ? window.location : global.location;
|
|
1041
|
-
current.children.push({
|
|
1042
|
-
|
|
981
|
+
// this.prevClassList = [];
|
|
1043
|
-
|
|
982
|
+
// this.shadow = this.attachShadow({ mode: 'open' });
|
|
1044
|
-
});
|
|
1045
|
-
|
|
983
|
+
}
|
|
1046
984
|
|
|
1047
|
-
// if we're at root, push new base node
|
|
1048
|
-
|
|
985
|
+
connectedCallback() {
|
|
986
|
+
this._connected = true;
|
|
987
|
+
this.state.subscribe(() => this.update());
|
|
1049
|
-
|
|
988
|
+
this.update();
|
|
1050
|
-
|
|
989
|
+
}
|
|
1051
990
|
|
|
991
|
+
disconnectedCallback() {
|
|
1052
|
-
|
|
992
|
+
this._connected = false;
|
|
993
|
+
this.state.unsubscribe(() => this.update());
|
|
994
|
+
}
|
|
1053
995
|
|
|
996
|
+
attributeChangedCallback(key, oldValue, newValue) {
|
|
997
|
+
this.attrs[key] = newValue && newValue.startsWith(`{`) ? JSON.parse(newValue.replace(/'/g, `"`)) : newValue;
|
|
1054
|
-
if (
|
|
998
|
+
if (this._connected) {
|
|
1055
|
-
|
|
999
|
+
this.update();
|
|
1056
1000
|
}
|
|
1057
|
-
|
|
1058
|
-
arr[level] = current;
|
|
1059
1001
|
}
|
|
1060
1002
|
|
|
1003
|
+
update() {
|
|
1061
|
-
|
|
1004
|
+
if (this._dirty) {
|
|
1062
|
-
if (level > -1 && (current.voidElement || current.name === tag.slice(2, -1))) {
|
|
1063
|
-
|
|
1005
|
+
return;
|
|
1064
|
-
// move current up a level to match the end tag
|
|
1065
|
-
current = level === -1 ? result : arr[level];
|
|
1066
1006
|
}
|
|
1067
|
-
|
|
1007
|
+
this._dirty = true;
|
|
1068
|
-
|
|
1008
|
+
this.enqueueUpdate();
|
|
1069
|
-
// if we're at the root, push a base text node. otherwise add as
|
|
1070
|
-
|
|
1009
|
+
}
|
|
1071
|
-
parent = level === -1 ? result : arr[level].children;
|
|
1072
1010
|
|
|
1073
|
-
// calculate correct end of the content slice in case there's
|
|
1074
|
-
|
|
1011
|
+
_performUpdate() {
|
|
1075
|
-
const end = html.indexOf('<', start);
|
|
1076
|
-
let content = html.slice(start, end === -1 ? undefined : end);
|
|
1077
|
-
// if a node is nothing but whitespace, collapse it as the spec states:
|
|
1078
|
-
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
|
|
1079
|
-
|
|
1012
|
+
if (!this._connected) {
|
|
1080
|
-
content = ' ';
|
|
1081
|
-
}
|
|
1082
|
-
// don't add whitespace-only text nodes if they would be trailing text nodes
|
|
1083
|
-
// or if they would be leading whitespace-only text nodes:
|
|
1084
|
-
// * end > -1 indicates this is not a trailing text node
|
|
1085
|
-
// * leading node is when level is -1 and parent has length 0
|
|
1086
|
-
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
|
|
1087
|
-
|
|
1013
|
+
return;
|
|
1088
|
-
type: 'text',
|
|
1089
|
-
content: content,
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
1014
|
}
|
|
1015
|
+
this.render();
|
|
1016
|
+
this._dirty = false;
|
|
1093
1017
|
}
|
|
1094
|
-
});
|
|
1095
1018
|
|
|
1019
|
+
batch(runner, pick, callback) {
|
|
1096
|
-
|
|
1020
|
+
const q = [];
|
|
1021
|
+
const flush = () => {
|
|
1022
|
+
let p;
|
|
1023
|
+
while ((p = pick(q))) callback(p);
|
|
1097
|
-
};
|
|
1024
|
+
};
|
|
1098
|
-
|
|
1099
|
-
const stringifyAttrs = (attrs) => {
|
|
1100
|
-
|
|
1025
|
+
const run = runner(flush);
|
|
1101
|
-
for (let key in attrs) {
|
|
1102
|
-
|
|
1026
|
+
q.push(this) === 1 && run();
|
|
1103
|
-
|
|
1027
|
+
}
|
|
1104
|
-
if (!buff.length) {
|
|
1105
|
-
return '';
|
|
1106
|
-
}
|
|
1107
|
-
return ' ' + buff.join(' ');
|
|
1108
|
-
};
|
|
1109
1028
|
|
|
1110
|
-
const stringifyHtml = (buff, doc) => {
|
|
1111
|
-
|
|
1029
|
+
enqueueUpdate() {
|
|
1112
|
-
case 'text':
|
|
1113
|
-
|
|
1030
|
+
this.batch(microtask, fifo, () => this._performUpdate());
|
|
1114
|
-
case 'tag':
|
|
1115
|
-
buff += '<' + doc.name + (doc.attrs ? stringifyAttrs(doc.attrs) : '') + (doc.voidElement ? '/>' : '>');
|
|
1116
|
-
if (doc.voidElement) {
|
|
1117
|
-
return buff;
|
|
1118
|
-
|
|
1031
|
+
}
|
|
1119
|
-
return buff + doc.children.reduce(stringifyHtml, '') + '</' + doc.name + '>';
|
|
1120
|
-
case 'comment':
|
|
1121
|
-
buff += '<!--' + doc.comment + '-->';
|
|
1122
|
-
return buff;
|
|
1123
|
-
}
|
|
1124
|
-
};
|
|
1125
1032
|
|
|
1033
|
+
render() {
|
|
1126
|
-
const
|
|
1034
|
+
const template = renderFn({
|
|
1035
|
+
attrs: this.attrs,
|
|
1127
|
-
|
|
1036
|
+
state: this.state.getValue(),
|
|
1128
|
-
if (Clazz) {
|
|
1129
|
-
|
|
1037
|
+
actions: this.state.actions,
|
|
1130
|
-
|
|
1038
|
+
});
|
|
1131
|
-
const attrType = Clazz.attrTypes[key];
|
|
1132
|
-
if (
|
|
1039
|
+
if (isBrowser) {
|
|
1040
|
+
// TODO: this can be optimized when we know whether the value belongs in a class (AttributePart)
|
|
1041
|
+
// maybe do this in lit-html itselfs
|
|
1042
|
+
const newClassList = getClassList(template).filter((cls) => {
|
|
1043
|
+
const globalStyles = document.getElementById('global').textContent;
|
|
1044
|
+
return !globalStyles.includes('.' + cls);
|
|
1045
|
+
});
|
|
1046
|
+
if (newClassList.length > 0) {
|
|
1047
|
+
document.getElementById('global').textContent += generateTWStyleSheet(newClassList);
|
|
1048
|
+
}
|
|
1049
|
+
renderHtml(template, this);
|
|
1050
|
+
// For shadows only
|
|
1051
|
+
// if (!this.styleElement) {
|
|
1052
|
+
// render(template, this.shadow);
|
|
1053
|
+
// const styleSheet = generateTWStyleSheet(classList);
|
|
1133
|
-
|
|
1054
|
+
// this.prevClassList = classList;
|
|
1055
|
+
// this.styleElement = document.createElement('style');
|
|
1056
|
+
// this.shadow.appendChild(this.styleElement).textContent = css(pageStyles) + styleSheet;
|
|
1057
|
+
// } else {
|
|
1058
|
+
// const missingClassList = classList.filter((cls) => !this.prevClassList.includes(cls));
|
|
1059
|
+
// if (missingClassList.length > 0) {
|
|
1060
|
+
// const styleSheet = generateTWStyleSheet(missingClassList);
|
|
1061
|
+
// this.styleElement.textContent += '\n' + styleSheet;
|
|
1062
|
+
// this.prevClassList.push(...missingClassList);
|
|
1063
|
+
// }
|
|
1064
|
+
// render(template, this.shadow);
|
|
1065
|
+
// }
|
|
1134
1066
|
} else {
|
|
1135
|
-
|
|
1067
|
+
return renderHtml(template);
|
|
1136
1068
|
}
|
|
1137
|
-
});
|
|
1138
|
-
const instance = new Clazz(newAttrs);
|
|
1139
|
-
const res = instance.renderTemplate();
|
|
1140
|
-
node.children = parseHtml(res);
|
|
1141
|
-
}
|
|
1142
|
-
if (node.children) {
|
|
1143
|
-
for (const child of node.children) {
|
|
1144
|
-
hydrate(child);
|
|
1145
1069
|
}
|
|
1146
|
-
}
|
|
1147
|
-
};
|
|
1070
|
+
};
|
|
1148
|
-
|
|
1149
|
-
const wrapAttribute = (attrName, suffix, text, v) => {
|
|
1150
|
-
let buffer = text;
|
|
1151
|
-
const hasQuote = suffix && suffix.includes(`="`);
|
|
1152
|
-
|
|
1071
|
+
registerElement(name, Element);
|
|
1153
|
-
buffer += `"`;
|
|
1154
|
-
}
|
|
1155
|
-
buffer += v;
|
|
1156
|
-
|
|
1072
|
+
return { name, attrs, state, render: renderFn };
|
|
1157
|
-
buffer += `"`;
|
|
1158
|
-
}
|
|
1159
|
-
return buffer;
|
|
1160
1073
|
};
|
|
1161
1074
|
|
|
1162
|
-
export const render = isBrowser
|
|
1163
|
-
? litRender
|
|
1164
|
-
: (template) => {
|
|
1165
|
-
let js = '';
|
|
1166
|
-
template.strings.forEach((text, i) => {
|
|
1167
|
-
const value = template.values[i];
|
|
1168
|
-
const type = typeof value;
|
|
1169
|
-
let attrName, suffix;
|
|
1170
|
-
const matchName = lastAttributeNameRegex.exec(text);
|
|
1171
|
-
if (matchName) {
|
|
1172
|
-
attrName = matchName[2];
|
|
1173
|
-
suffix = matchName[3];
|
|
1174
|
-
}
|
|
1175
|
-
if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
|
|
1176
|
-
js += wrapAttribute(attrName, suffix, text, type !== 'string' ? String(value) : value);
|
|
1177
|
-
} else if (Array.isArray(value) && value.find((item) => item && item.strings && item.type === 'html')) {
|
|
1178
|
-
js += text;
|
|
1179
|
-
value.forEach((v) => {
|
|
1180
|
-
js += render(v);
|
|
1181
|
-
});
|
|
1182
|
-
} else if (type === 'object') {
|
|
1183
|
-
// TemplateResult
|
|
1184
|
-
if (value.strings && value.type === 'html') {
|
|
1185
|
-
js += text;
|
|
1186
|
-
js += render(value);
|
|
1187
|
-
} else {
|
|
1188
|
-
js += wrapAttribute(attrName, suffix, text, JSON.stringify(value).replace(/"/g, `'`));
|
|
1189
|
-
}
|
|
1190
|
-
} else if (type == 'function') {
|
|
1191
|
-
if (attrName) {
|
|
1192
|
-
js += text.replace(' ' + attrName + '=', '');
|
|
1193
|
-
} else {
|
|
1194
|
-
// js += text;
|
|
1195
|
-
// js += value();
|
|
1196
|
-
}
|
|
1197
|
-
} else if (type !== 'undefined') {
|
|
1198
|
-
js += text;
|
|
1199
|
-
js += value.toString();
|
|
1200
|
-
} else {
|
|
1201
|
-
js += text;
|
|
1202
|
-
// console.log('value', value);
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
const nodes = parseHtml(js);
|
|
1206
|
-
for (const node of nodes) {
|
|
1207
|
-
hydrate(node);
|
|
1208
|
-
}
|
|
1209
|
-
const html = nodes.reduce((acc, node) => {
|
|
1210
|
-
return acc + stringifyHtml('', node);
|
|
1211
|
-
}, '');
|
|
1212
|
-
return html;
|
|
1213
|
-
};
|
|
1214
|
-
|
|
1215
1075
|
export const createPage = ({ head, body }) => {
|
|
1216
1076
|
return ({ headScript, bodyScript, lang, props }) => {
|
|
1217
|
-
const isProd = process.env.NODE_ENV === 'production';
|
|
1218
|
-
const headHtml =
|
|
1077
|
+
const headHtml = renderHtml(head(props));
|
|
1219
|
-
const bodyHtml =
|
|
1078
|
+
const bodyHtml = renderHtml(body(props));
|
|
1220
1079
|
const classes = extractClasses(bodyHtml);
|
|
1221
1080
|
return `
|
|
1222
1081
|
<!DOCTYPE html>
|
|
@@ -1238,7 +1097,6 @@ export const createPage = ({ head, body }) => {
|
|
|
1238
1097
|
<body>
|
|
1239
1098
|
${bodyHtml}
|
|
1240
1099
|
<script>
|
|
1241
|
-
window.__DEV__ = ${!isProd};
|
|
1242
1100
|
window.props = ${JSON.stringify(props)};
|
|
1243
1101
|
</script>
|
|
1244
1102
|
${bodyScript}
|
index.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test, jest } from '@jest/globals';
|
|
2
|
-
import { AtomsElement, createElement, createPage, html, render, number, boolean, string, array, object, unsafeHTML, css } from './index.js';
|
|
2
|
+
import { AtomsElement, createElement, createPage, createState, html, render, number, boolean, string, array, object, unsafeHTML, css } from './index.js';
|
|
3
3
|
|
|
4
4
|
global.__DEV = true;
|
|
5
5
|
|
|
@@ -50,6 +50,40 @@ primitives.forEach((value) =>
|
|
|
50
50
|
}),
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
+
test.only('createState', () => {
|
|
54
|
+
// computed: {
|
|
55
|
+
// sum: (state, v) => state.count + v,
|
|
56
|
+
// },
|
|
57
|
+
const countState = createState({
|
|
58
|
+
state: {
|
|
59
|
+
count: 0,
|
|
60
|
+
},
|
|
61
|
+
reducer: {
|
|
62
|
+
increment: (state, a) => ({ ...state, count: state.count + a }),
|
|
63
|
+
decrement: (state, a) => ({ ...state, count: state.count - a }),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
createElement({
|
|
67
|
+
name: 'hello',
|
|
68
|
+
attrs: {
|
|
69
|
+
name: '123',
|
|
70
|
+
},
|
|
71
|
+
state: countState,
|
|
72
|
+
render: ({ attrs, state, actions }) => {
|
|
73
|
+
console.log('attrs', attrs);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// countState.subscribe((v) => {
|
|
77
|
+
// console.log(v);
|
|
78
|
+
// });
|
|
79
|
+
// countState.increment(4);
|
|
80
|
+
// console.log(countState.sum(21));
|
|
81
|
+
// countState.decrement(1);
|
|
82
|
+
// console.log(countState.sum(21));
|
|
83
|
+
// countState.decrement(2);
|
|
84
|
+
// console.log(countState.sum(21));
|
|
85
|
+
});
|
|
86
|
+
|
|
53
87
|
test('object', () => {
|
|
54
88
|
const spy = jest.spyOn(global.console, 'warn').mockImplementation();
|
|
55
89
|
const context = 'data';
|