atoms-element v5.0.0
git clone https://git.pyrossh.dev/atoms-element
A simple web component library for defining your custom elements. It works on both client and server.
4c11234
— Peter John
2026-08-07T11:52:39+05:30
improve rendering
Generated files not shown: package-lock.json
- __snapshots__/index.test.js.snap +0 -352
- examples/e2e.spec.js +68 -0
- examples/server.js +33 -3
- index.js +14 -20
- index.test.js +47 -48
- index.test.js.snapshot +47 -0
- lit-html.js +0 -1244
- package.json +7 -8
__snapshots__/index.test.js.snap
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`createElement styles are scoped and injected via SSR 1`] = `
|
|
4
|
-
"@scope (styled-element) {
|
|
5
|
-
|
|
6
|
-
:scope {
|
|
7
|
-
display: flex;
|
|
8
|
-
}
|
|
9
|
-
.count {
|
|
10
|
-
color: magenta;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
"
|
|
15
|
-
`;
|
|
16
|
-
|
|
17
|
-
exports[`createElement with attrs and hooks 1`] = `
|
|
18
|
-
"
|
|
19
|
-
<div>
|
|
20
|
-
<div>
|
|
21
|
-
<span>perPage: 5</span> </div> </div> <span>Count: 3</span> <button>Set</button> "
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
exports[`createElement without attrs 1`] = `" <div></div>"`;
|
|
25
|
-
|
|
26
|
-
exports[`createPage 1`] = `
|
|
27
|
-
"
|
|
28
|
-
<!DOCTYPE html>
|
|
29
|
-
<html lang=\\"en\\">
|
|
30
|
-
<head>
|
|
31
|
-
<meta charset=\\"utf-8\\" />
|
|
32
|
-
<meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\" />
|
|
33
|
-
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">
|
|
34
|
-
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no\\">
|
|
35
|
-
<link rel=\\"sitemap\\" type=\\"application/xml\\" href=\\"/sitemap.xml\\" />
|
|
36
|
-
<link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />
|
|
37
|
-
|
|
38
|
-
<title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>
|
|
39
|
-
<style id=\\"global\\">
|
|
40
|
-
*, ::before, ::after {
|
|
41
|
-
|
|
42
|
-
box-sizing: border-box;
|
|
43
|
-
border-width: 0;
|
|
44
|
-
border-style: solid;
|
|
45
|
-
border-color: #e5e7eb;
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
hr {
|
|
49
|
-
|
|
50
|
-
height: 0;
|
|
51
|
-
color: inherit;
|
|
52
|
-
border-top-width: 1px;
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
abbr[title] {
|
|
56
|
-
|
|
57
|
-
-webkit-text-decoration: underline dotted;
|
|
58
|
-
text-decoration: underline dotted;
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
b, strong {
|
|
62
|
-
|
|
63
|
-
font-weight: bolder;
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
code, kbd, samp, pre {
|
|
67
|
-
|
|
68
|
-
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
69
|
-
font-size: 1em;
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
small {
|
|
73
|
-
|
|
74
|
-
font-size: 80%;
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
sub, sup {
|
|
78
|
-
|
|
79
|
-
font-size: 75%;
|
|
80
|
-
line-height: 0;
|
|
81
|
-
position: relative;
|
|
82
|
-
vertical-align: baseline;
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
sub {
|
|
86
|
-
|
|
87
|
-
bottom: -0.25em;
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
sup {
|
|
91
|
-
|
|
92
|
-
top: -0.5em;
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
table {
|
|
96
|
-
|
|
97
|
-
text-indent: 0;
|
|
98
|
-
border-color: inherit;
|
|
99
|
-
border-collapse: collapse;
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
button, input, optgroup, select, textarea {
|
|
103
|
-
|
|
104
|
-
font-size: 100%;
|
|
105
|
-
margin: 0;
|
|
106
|
-
padding: 0;
|
|
107
|
-
line-height: inherit;
|
|
108
|
-
color: inherit;
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
button, select {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
button, [type='button'], [type='reset'], [type='submit'] {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
::-moz-focus-inner {
|
|
120
|
-
|
|
121
|
-
border-style: none;
|
|
122
|
-
padding: 0;
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
:-moz-focusring {
|
|
126
|
-
|
|
127
|
-
outline: 1px dotted ButtonText;
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
:-moz-ui-invalid {
|
|
131
|
-
|
|
132
|
-
box-shadow: none;
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
legend {
|
|
136
|
-
|
|
137
|
-
padding: 0;
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
progress {
|
|
141
|
-
|
|
142
|
-
vertical-align: baseline;
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
|
146
|
-
|
|
147
|
-
height: auto;
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
[type='search'] {
|
|
151
|
-
|
|
152
|
-
-webkit-appearance: textfield;
|
|
153
|
-
outline-offset: -2px;
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
::-webkit-search-decoration {
|
|
157
|
-
|
|
158
|
-
-webkit-appearance: none;
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
::-webkit-file-upload-button {
|
|
162
|
-
|
|
163
|
-
-webkit-appearance: button;
|
|
164
|
-
font: inherit;
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
summary {
|
|
168
|
-
|
|
169
|
-
display: list-item;
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {
|
|
173
|
-
|
|
174
|
-
margin: 0;
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
button {
|
|
178
|
-
|
|
179
|
-
background-image: none;
|
|
180
|
-
|
|
181
|
-
outline: 1px dotted, 5px auto -webkit-focus-ring-color;
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
fieldset {
|
|
185
|
-
|
|
186
|
-
margin: 0;
|
|
187
|
-
padding: 0;
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
ol, ul {
|
|
191
|
-
|
|
192
|
-
list-style: none;
|
|
193
|
-
margin: 0;
|
|
194
|
-
padding: 0;
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
img {
|
|
198
|
-
|
|
199
|
-
border-style: solid;
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
textarea {
|
|
203
|
-
|
|
204
|
-
resize: vertical;
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
input::-moz-placeholder, textarea::-moz-placeholder {
|
|
208
|
-
|
|
209
|
-
opacity: 1;
|
|
210
|
-
color: #9ca3af;
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
input:-ms-input-placeholder, textarea:-ms-input-placeholder {
|
|
214
|
-
|
|
215
|
-
opacity: 1;
|
|
216
|
-
color: #9ca3af;
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
input::placeholder, textarea::placeholder {
|
|
220
|
-
|
|
221
|
-
opacity: 1;
|
|
222
|
-
color: #9ca3af;
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
button, [role='button'] {
|
|
226
|
-
|
|
227
|
-
cursor: pointer;
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
h1, h2, h3, h4, h5, h6 {
|
|
231
|
-
|
|
232
|
-
font-size: inherit;
|
|
233
|
-
font-weight: inherit;
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
a {
|
|
237
|
-
|
|
238
|
-
color: inherit;
|
|
239
|
-
text-decoration: inherit;
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
pre, code, kbd, samp {
|
|
243
|
-
|
|
244
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
img, svg, video, canvas, audio, iframe, embed, object {
|
|
248
|
-
|
|
249
|
-
display: block;
|
|
250
|
-
vertical-align: middle;
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
img, video {
|
|
254
|
-
|
|
255
|
-
max-width: 100%;
|
|
256
|
-
height: auto;
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
html {
|
|
260
|
-
|
|
261
|
-
-moz-tab-size: 4;
|
|
262
|
-
-o-tab-size: 4;
|
|
263
|
-
tab-size: 4;
|
|
264
|
-
line-height: 1.5;
|
|
265
|
-
-webkit-text-size-adjust: 100%;
|
|
266
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
267
|
-
width: 100%;
|
|
268
|
-
height: 100%;
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
body {
|
|
272
|
-
|
|
273
|
-
margin: 0px;
|
|
274
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|
275
|
-
line-height: 1.4;
|
|
276
|
-
background-color: white;
|
|
277
|
-
width: 100%;
|
|
278
|
-
height: 100%;
|
|
279
|
-
display: flex;
|
|
280
|
-
flex-direction: column;
|
|
281
|
-
flex: 1 1 0%;
|
|
282
|
-
min-width: 320px;
|
|
283
|
-
min-height: 100vh;
|
|
284
|
-
font-weight: 400;
|
|
285
|
-
color: rgba(44, 62, 80, 1);
|
|
286
|
-
direction: ltr;
|
|
287
|
-
font-synthesis: none;
|
|
288
|
-
text-rendering: optimizeLegibility;
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
</style>
|
|
294
|
-
<script type=\\"module\\"><script>
|
|
295
|
-
</head>
|
|
296
|
-
<body>
|
|
297
|
-
|
|
298
|
-
<div>
|
|
299
|
-
<app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">
|
|
300
|
-
<h1 class=\\"text-5xl\\">123</h1> </main> </div>
|
|
301
|
-
<script>
|
|
302
|
-
window.props = {\\"config\\":{\\"title\\":\\"123\\"}};
|
|
303
|
-
</script>
|
|
304
|
-
<script type=\\"module\\"><script>
|
|
305
|
-
</body>
|
|
306
|
-
</html>
|
|
307
|
-
"
|
|
308
|
-
`;
|
|
309
|
-
|
|
310
|
-
exports[`css tagged template 1`] = `
|
|
311
|
-
"
|
|
312
|
-
:scope {
|
|
313
|
-
display: flex;
|
|
314
|
-
}
|
|
315
|
-
button {
|
|
316
|
-
color: magenta;
|
|
317
|
-
}
|
|
318
|
-
"
|
|
319
|
-
`;
|
|
320
|
-
|
|
321
|
-
exports[`render attribute keys 1`] = `
|
|
322
|
-
"
|
|
323
|
-
<div>
|
|
324
|
-
<app-counter name=\\"123\\" perPage=\\"1\\"></app-counter> </div>"
|
|
325
|
-
`;
|
|
326
|
-
|
|
327
|
-
exports[`render attributes within quotes 1`] = `
|
|
328
|
-
"
|
|
329
|
-
<div>
|
|
330
|
-
<app-counter name=\\"123\\" class=\\"high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
|
|
331
|
-
`;
|
|
332
|
-
|
|
333
|
-
exports[`render multi template 1`] = `
|
|
334
|
-
"
|
|
335
|
-
<div>
|
|
336
|
-
|
|
337
|
-
<app-item meta=\\"{'index':1}\\">
|
|
338
|
-
<button>+</button> </app-item> <app-item meta=\\"{'index':2}\\">
|
|
339
|
-
<button>+</button> </app-item> </div>"
|
|
340
|
-
`;
|
|
341
|
-
|
|
342
|
-
exports[`render single template 1`] = `" <div>NoCountry false</div>"`;
|
|
343
|
-
|
|
344
|
-
exports[`render unsafeHTML 1`] = `" <div><div><p class=\\"123\\">this is unsafe</p></div></div>"`;
|
|
345
|
-
|
|
346
|
-
exports[`renderHtml 1`] = `
|
|
347
|
-
"
|
|
348
|
-
<div>
|
|
349
|
-
<app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
|
|
350
|
-
`;
|
|
351
|
-
|
|
352
|
-
exports[`renderHtml escapes text and attribute values 1`] = `"<div class=\\"a" onmouseover="alert(1)\\"><script>alert(1)</script></div>"`;
|
examples/e2e.spec.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Real-browser test for the example app, run with: npm run test:e2e
|
|
2
|
+
// Requires Chromium once: npx playwright install chromium
|
|
3
|
+
// Named .spec.js (not .test.js) so it's never picked up by a bare `node --test`.
|
|
4
|
+
import { test, before, after } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { chromium } from 'playwright';
|
|
8
|
+
|
|
9
|
+
const PORT = 3987;
|
|
10
|
+
const BASE_URL = `http://localhost:${PORT}/`;
|
|
11
|
+
|
|
12
|
+
let serverProcess;
|
|
13
|
+
let browser;
|
|
14
|
+
let page;
|
|
15
|
+
const pageErrors = [];
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
serverProcess = spawn(process.execPath, ['examples/server.js', String(PORT)], { stdio: 'ignore' });
|
|
19
|
+
|
|
20
|
+
const deadline = Date.now() + 15000;
|
|
21
|
+
for (;;) {
|
|
22
|
+
try {
|
|
23
|
+
if ((await fetch(BASE_URL)).ok) break;
|
|
24
|
+
} catch {
|
|
25
|
+
// server not up yet
|
|
26
|
+
}
|
|
27
|
+
if (Date.now() > deadline) throw new Error('example server did not start in time');
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
browser = await chromium.launch();
|
|
32
|
+
page = await browser.newPage();
|
|
33
|
+
page.on('pageerror', (err) => pageErrors.push(err.message));
|
|
34
|
+
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
|
35
|
+
await page.waitForSelector('app-counter');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
after(async () => {
|
|
39
|
+
await browser?.close();
|
|
40
|
+
serverProcess?.kill();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('renders each counter exactly once, with no leftover duplicate SSR markup', async () => {
|
|
44
|
+
const counters = page.locator('app-counter');
|
|
45
|
+
assert.strictEqual(await counters.count(), 2);
|
|
46
|
+
for (let i = 0; i < 2; i++) {
|
|
47
|
+
assert.strictEqual(await counters.nth(i).locator('h1').count(), 1, `counter ${i} should render exactly one <h1>`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('clicking +/- updates the counter and the shared total reducer', async () => {
|
|
52
|
+
const first = page.locator('app-counter').nth(0);
|
|
53
|
+
const second = page.locator('app-counter').nth(1);
|
|
54
|
+
|
|
55
|
+
await first.getByRole('button', { name: '+' }).click();
|
|
56
|
+
await first.getByRole('button', { name: '+' }).click();
|
|
57
|
+
await first.getByRole('button', { name: '+' }).click();
|
|
58
|
+
await second.getByRole('button', { name: '+' }).click();
|
|
59
|
+
await first.getByRole('button', { name: '-' }).click();
|
|
60
|
+
|
|
61
|
+
assert.strictEqual((await first.locator('h1').textContent()).trim(), '2');
|
|
62
|
+
assert.strictEqual((await second.locator('h1').textContent()).trim(), '1');
|
|
63
|
+
assert.strictEqual((await page.locator('app-total h1').textContent()).trim(), 'Total of 2 Counters: 3');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('no console/page errors were thrown', () => {
|
|
67
|
+
assert.deepStrictEqual(pageErrors, []);
|
|
68
|
+
});
|
examples/server.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
import { fileURLToPath } from 'url';
|
|
4
5
|
import { dirname } from 'path';
|
|
5
6
|
import renderPage from './pages/index.js';
|
|
6
7
|
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
10
|
+
const rootDir = path.join(__dirname, '..');
|
|
9
11
|
const port = process.argv[2] || 3000;
|
|
10
12
|
const elements = ['app-counter.js', 'app-total.js'];
|
|
11
13
|
const srcMap = {
|
|
12
|
-
'/index.js': `${
|
|
14
|
+
'/index.js': `${rootDir}/index.js`,
|
|
13
|
-
'/lit-html.js': `${__dirname}/../lit-html.js`,
|
|
14
15
|
'/store.js': `${__dirname}/store.js`,
|
|
15
16
|
};
|
|
16
17
|
elements.forEach((el) => {
|
|
17
18
|
srcMap['/elements/' + el] = `${__dirname}/elements/${el}`;
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
// lit-html and mutative are real npm dependencies (not vendored), so the
|
|
22
|
+
// browser needs an import map for the bare specifiers index.js uses, plus
|
|
23
|
+
// the packages' files served as-is — their internal imports are all
|
|
24
|
+
// relative, so no other wiring is needed.
|
|
25
|
+
const importMap = {
|
|
26
|
+
imports: {
|
|
27
|
+
'lit-html': '/node_modules/lit-html/lit-html.js',
|
|
28
|
+
'lit-html/directive-helpers.js': '/node_modules/lit-html/directive-helpers.js',
|
|
29
|
+
'lit-html/directives/unsafe-html.js': '/node_modules/lit-html/directives/unsafe-html.js',
|
|
30
|
+
mutative: '/node_modules/mutative/dist/mutative.esm.mjs',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const nodeModulesDir = path.join(rootDir, 'node_modules');
|
|
34
|
+
|
|
20
35
|
http
|
|
21
36
|
.createServer((req, res) => {
|
|
22
37
|
if (req.url.includes('/api/posts')) {
|
|
@@ -40,7 +55,7 @@ http
|
|
|
40
55
|
props: {
|
|
41
56
|
config: { lang: 'en', title: 'Counter App' },
|
|
42
57
|
},
|
|
43
|
-
headScript:
|
|
58
|
+
headScript: `<script type="importmap">${JSON.stringify(importMap)}</script>`,
|
|
44
59
|
bodyScript: `
|
|
45
60
|
<script type="module">
|
|
46
61
|
${elements.map((el) => `import './elements/${el}';`).join('\n')}
|
|
@@ -50,12 +65,27 @@ http
|
|
|
50
65
|
res.end(html);
|
|
51
66
|
return;
|
|
52
67
|
}
|
|
68
|
+
if (req.url.startsWith('/node_modules/')) {
|
|
69
|
+
const relative = req.url.slice('/node_modules/'.length);
|
|
70
|
+
const filename = path.normalize(path.join(nodeModulesDir, relative));
|
|
71
|
+
if (filename.startsWith(nodeModulesDir) && fs.existsSync(filename)) {
|
|
72
|
+
res.setHeader('Content-type', 'application/javascript');
|
|
73
|
+
res.end(fs.readFileSync(filename));
|
|
74
|
+
} else {
|
|
75
|
+
res.statusCode = 404;
|
|
76
|
+
res.end();
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
53
80
|
const filename = srcMap[req.url];
|
|
54
81
|
if (filename) {
|
|
55
82
|
const data = fs.readFileSync(filename);
|
|
56
83
|
res.setHeader('Content-type', 'application/javascript');
|
|
57
84
|
res.end(data);
|
|
85
|
+
return;
|
|
58
86
|
}
|
|
87
|
+
res.statusCode = 404;
|
|
88
|
+
res.end();
|
|
59
89
|
})
|
|
60
90
|
.listen(parseInt(port));
|
|
61
91
|
|
index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { html, render as litRender
|
|
1
|
+
import { html, render as litRender } from 'lit-html';
|
|
2
|
+
import { isTemplateResult } from 'lit-html/directive-helpers.js';
|
|
3
|
+
import { unsafeHTML as litUnsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
|
2
4
|
import { create } from 'mutative';
|
|
3
5
|
|
|
4
6
|
const isBrowser = typeof window !== 'undefined';
|
|
@@ -255,14 +257,14 @@ export const renderHtml = isBrowser
|
|
|
255
257
|
} else if (value === null || !(type === 'object' || type === 'function' || type === 'undefined')) {
|
|
256
258
|
const str = type !== 'string' ? String(value) : value;
|
|
257
259
|
js += wrapAttribute(attrName, suffix, text, attrName ? escapeAttribute(str) : escapeText(str));
|
|
258
|
-
} else if (Array.isArray(value) && value.find((item) => item && item
|
|
260
|
+
} else if (Array.isArray(value) && value.find((item) => item && isTemplateResult(item))) {
|
|
259
261
|
js += text;
|
|
260
262
|
value.forEach((v) => {
|
|
261
263
|
js += renderHtml(v);
|
|
262
264
|
});
|
|
263
265
|
} else if (type === 'object') {
|
|
264
266
|
// TemplateResult
|
|
265
|
-
if (value
|
|
267
|
+
if (isTemplateResult(value)) {
|
|
266
268
|
js += text;
|
|
267
269
|
js += renderHtml(value);
|
|
268
270
|
} else {
|
|
@@ -434,23 +436,7 @@ const pageStyles = {
|
|
|
434
436
|
},
|
|
435
437
|
};
|
|
436
438
|
|
|
437
|
-
const previousValues = new WeakMap();
|
|
438
|
-
export const unsafeHTML = isBrowser
|
|
439
|
-
? directive((value) => (part) => {
|
|
440
|
-
if (!(part instanceof NodePart)) {
|
|
441
|
-
throw new Error('unsafeHTML can only be used in text bindings');
|
|
442
|
-
}
|
|
443
|
-
const previousValue = previousValues.get(part);
|
|
444
|
-
if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
const template = document.createElement('template');
|
|
448
|
-
template.innerHTML = value; // innerHTML casts to string internally
|
|
449
|
-
const fragment = document.importNode(template.content, true);
|
|
450
|
-
part.setValue(fragment);
|
|
451
|
-
previousValues.set(part, { value, fragment });
|
|
452
|
-
})
|
|
453
|
-
|
|
439
|
+
export const unsafeHTML = isBrowser ? litUnsafeHTML : (value) => ({ [UNSAFE_HTML]: true, value });
|
|
454
440
|
|
|
455
441
|
const fifo = (q) => q.shift();
|
|
456
442
|
const filo = (q) => q.pop();
|
|
@@ -557,6 +543,7 @@ export const createElement = (meta, renderFn) => {
|
|
|
557
543
|
super();
|
|
558
544
|
this._dirty = false;
|
|
559
545
|
this._connected = false;
|
|
546
|
+
this._hydrated = false;
|
|
560
547
|
this.attrs = ssrAttrs || {};
|
|
561
548
|
this.state = {};
|
|
562
549
|
this.hooks = {
|
|
@@ -655,6 +642,13 @@ export const createElement = (meta, renderFn) => {
|
|
|
655
642
|
injectedStyleTags.add(name);
|
|
656
643
|
document.getElementById('global').textContent += styles;
|
|
657
644
|
}
|
|
645
|
+
if (!this._hydrated) {
|
|
646
|
+
// lit-html's render() appends into the container rather than replacing its
|
|
647
|
+
// content, so any server-rendered light-DOM markup must be discarded before
|
|
648
|
+
// the first client-side render or it's left behind as an inert duplicate.
|
|
649
|
+
this._hydrated = true;
|
|
650
|
+
this.textContent = '';
|
|
651
|
+
}
|
|
658
652
|
renderHtml(template, this);
|
|
659
653
|
} else {
|
|
660
654
|
return renderHtml(template);
|
index.test.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
1
|
-
import
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
2
3
|
import { getElement, createElement, createPage, createReducer, html, renderHtml, unsafeHTML, css, useReducer, useEffect } from './index.js';
|
|
3
4
|
|
|
4
|
-
global.__DEV = true;
|
|
5
|
-
|
|
6
|
-
test('css tagged template', () => {
|
|
5
|
+
test('css tagged template', (t) => {
|
|
7
6
|
const color = 'magenta';
|
|
8
7
|
const styles = css`
|
|
9
8
|
:scope {
|
|
@@ -13,10 +12,10 @@ test('css tagged template', () => {
|
|
|
13
12
|
color: ${color};
|
|
14
13
|
}
|
|
15
14
|
`;
|
|
16
|
-
|
|
15
|
+
t.assert.snapshot(styles);
|
|
17
16
|
});
|
|
18
17
|
|
|
19
|
-
test('createElement styles are scoped and injected via SSR', () => {
|
|
18
|
+
test('createElement styles are scoped and injected via SSR', (t) => {
|
|
20
19
|
const Styled = () => {
|
|
21
20
|
return html` <div class="count"></div> `;
|
|
22
21
|
};
|
|
@@ -30,10 +29,10 @@ test('createElement styles are scoped and injected via SSR', () => {
|
|
|
30
29
|
`;
|
|
31
30
|
createElement({ url: '/styled-element.js' }, Styled);
|
|
32
31
|
const Clazz = getElement('styled-element');
|
|
33
|
-
|
|
32
|
+
t.assert.snapshot(Clazz.styles);
|
|
34
33
|
});
|
|
35
34
|
|
|
36
|
-
test('renderHtml', () => {
|
|
35
|
+
test('renderHtml', (t) => {
|
|
37
36
|
const age = 1;
|
|
38
37
|
const data = { name: '123', address: { street: '1' } };
|
|
39
38
|
const items = [1, 2, 3];
|
|
@@ -44,20 +43,20 @@ test('renderHtml', () => {
|
|
|
44
43
|
</div>
|
|
45
44
|
`;
|
|
46
45
|
const res = renderHtml(template);
|
|
47
|
-
|
|
46
|
+
t.assert.snapshot(res);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
|
-
test('render attribute keys', () => {
|
|
49
|
+
test('render attribute keys', (t) => {
|
|
51
50
|
const template = html`
|
|
52
51
|
<div>
|
|
53
52
|
<app-counter name="123" perPage="1"></app-counter>
|
|
54
53
|
</div>
|
|
55
54
|
`;
|
|
56
55
|
const res = renderHtml(template);
|
|
57
|
-
|
|
56
|
+
t.assert.snapshot(res);
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
test('render attributes within quotes', () => {
|
|
59
|
+
test('render attributes within quotes', (t) => {
|
|
61
60
|
const age = 1;
|
|
62
61
|
const data = { name: '123', address: { street: '1' } };
|
|
63
62
|
const items = [1, 2, 3];
|
|
@@ -68,33 +67,33 @@ test('render attributes within quotes', () => {
|
|
|
68
67
|
</div>
|
|
69
68
|
`;
|
|
70
69
|
const res = renderHtml(template);
|
|
71
|
-
|
|
70
|
+
t.assert.snapshot(res);
|
|
72
71
|
});
|
|
73
72
|
|
|
74
|
-
test('render unsafeHTML', () => {
|
|
73
|
+
test('render unsafeHTML', (t) => {
|
|
75
74
|
const textContent = `<div><p class="123">this is unsafe</p></div>`;
|
|
76
75
|
const template = html` <div>${unsafeHTML(textContent)}</div> `;
|
|
77
76
|
const res = renderHtml(template);
|
|
78
|
-
|
|
77
|
+
t.assert.snapshot(res);
|
|
79
78
|
});
|
|
80
79
|
|
|
81
|
-
test('renderHtml escapes text and attribute values', () => {
|
|
80
|
+
test('renderHtml escapes text and attribute values', (t) => {
|
|
82
81
|
const evil = '<script>alert(1)</script>';
|
|
83
82
|
const quote = 'a" onmouseover="alert(1)';
|
|
84
83
|
const template = html`<div class="${quote}">${evil}</div>`;
|
|
85
84
|
const res = renderHtml(template);
|
|
86
|
-
|
|
85
|
+
assert.ok(!res.includes('<script>'));
|
|
87
|
-
|
|
86
|
+
assert.ok(!res.includes('onmouseover="alert(1)"'));
|
|
88
|
-
|
|
87
|
+
t.assert.snapshot(res);
|
|
89
88
|
});
|
|
90
89
|
|
|
91
|
-
test('render single template', () => {
|
|
90
|
+
test('render single template', (t) => {
|
|
92
91
|
const template = html` <div>${html`NoCountry ${false}`}</div> `;
|
|
93
92
|
const res = renderHtml(template);
|
|
94
|
-
|
|
93
|
+
t.assert.snapshot(res);
|
|
95
94
|
});
|
|
96
95
|
|
|
97
|
-
test('render multi template', () => {
|
|
96
|
+
test('render multi template', (t) => {
|
|
98
97
|
const template = html`
|
|
99
98
|
<div>
|
|
100
99
|
${[1, 2].map(
|
|
@@ -107,10 +106,10 @@ test('render multi template', () => {
|
|
|
107
106
|
</div>
|
|
108
107
|
`;
|
|
109
108
|
const res = renderHtml(template);
|
|
110
|
-
|
|
109
|
+
t.assert.snapshot(res);
|
|
111
110
|
});
|
|
112
111
|
|
|
113
|
-
test('createReducer', () => {
|
|
112
|
+
test('createReducer', (t) => {
|
|
114
113
|
const countReducer = createReducer({
|
|
115
114
|
initial: {
|
|
116
115
|
count: 0,
|
|
@@ -125,24 +124,24 @@ test('createReducer', () => {
|
|
|
125
124
|
},
|
|
126
125
|
},
|
|
127
126
|
});
|
|
128
|
-
const mock =
|
|
127
|
+
const mock = t.mock.fn();
|
|
129
128
|
countReducer.subscribe(mock);
|
|
130
129
|
const before = countReducer.getValue();
|
|
131
130
|
countReducer.actions.increment(4);
|
|
132
|
-
|
|
131
|
+
assert.strictEqual(countReducer.getValue().count, 4);
|
|
133
|
-
|
|
132
|
+
assert.deepStrictEqual(mock.mock.calls[0].arguments, [{ count: 4, other: { untouched: true } }]);
|
|
134
133
|
countReducer.actions.decrement(1);
|
|
135
|
-
|
|
134
|
+
assert.strictEqual(countReducer.getValue().count, 3);
|
|
136
|
-
|
|
135
|
+
assert.deepStrictEqual(mock.mock.calls[1].arguments, [{ count: 3, other: { untouched: true } }]);
|
|
137
136
|
countReducer.actions.decrement(2);
|
|
138
137
|
const after = countReducer.getValue();
|
|
139
|
-
|
|
138
|
+
assert.strictEqual(after.count, 1);
|
|
140
|
-
|
|
139
|
+
assert.deepStrictEqual(mock.mock.calls[2].arguments, [{ count: 1, other: { untouched: true } }]);
|
|
141
|
-
|
|
140
|
+
assert.notStrictEqual(after, before);
|
|
142
|
-
|
|
141
|
+
assert.strictEqual(after.other, before.other);
|
|
143
142
|
});
|
|
144
143
|
|
|
145
|
-
test('useReducer selector only updates when the selected slice changes', () => {
|
|
144
|
+
test('useReducer selector only updates when the selected slice changes', (t) => {
|
|
146
145
|
const store = createReducer({
|
|
147
146
|
initial: { count: 0, other: 0 },
|
|
148
147
|
reducer: {
|
|
@@ -161,18 +160,18 @@ test('useReducer selector only updates when the selected slice changes', () => {
|
|
|
161
160
|
const Clazz = getElement('count-only');
|
|
162
161
|
const instance = new Clazz();
|
|
163
162
|
instance.render();
|
|
164
|
-
const updateSpy =
|
|
163
|
+
const updateSpy = t.mock.fn();
|
|
165
164
|
instance.update = updateSpy;
|
|
166
165
|
|
|
167
166
|
store.actions.incOther();
|
|
168
|
-
|
|
167
|
+
assert.strictEqual(updateSpy.mock.callCount(), 0);
|
|
169
168
|
|
|
170
169
|
store.actions.incCount();
|
|
171
|
-
|
|
170
|
+
assert.strictEqual(updateSpy.mock.callCount(), 1);
|
|
172
171
|
});
|
|
173
172
|
|
|
174
|
-
test('useEffect cleanup runs on disconnect', () => {
|
|
173
|
+
test('useEffect cleanup runs on disconnect', (t) => {
|
|
175
|
-
const cleanup =
|
|
174
|
+
const cleanup = t.mock.fn();
|
|
176
175
|
createElement({ url: '/cleanup-element.js' }, () => {
|
|
177
176
|
useEffect(() => cleanup, []);
|
|
178
177
|
return html` <div></div> `;
|
|
@@ -181,23 +180,23 @@ test('useEffect cleanup runs on disconnect', () => {
|
|
|
181
180
|
const instance = new Clazz();
|
|
182
181
|
instance.render();
|
|
183
182
|
instance._flushEffects();
|
|
184
|
-
|
|
183
|
+
assert.strictEqual(cleanup.mock.callCount(), 0);
|
|
185
184
|
|
|
186
185
|
instance.disconnectedCallback();
|
|
187
|
-
|
|
186
|
+
assert.strictEqual(cleanup.mock.callCount(), 1);
|
|
188
187
|
});
|
|
189
188
|
|
|
190
|
-
test('createElement without attrs', () => {
|
|
189
|
+
test('createElement without attrs', (t) => {
|
|
191
190
|
createElement({ url: '/base-element.js' }, () => {
|
|
192
191
|
return html` <div></div> `;
|
|
193
192
|
});
|
|
194
193
|
const Clazz = getElement('base-element');
|
|
195
194
|
const instance = new Clazz();
|
|
196
195
|
const res = instance.render();
|
|
197
|
-
|
|
196
|
+
t.assert.snapshot(res);
|
|
198
197
|
});
|
|
199
198
|
|
|
200
|
-
test('createElement with attrs and hooks', () => {
|
|
199
|
+
test('createElement with attrs and hooks', (t) => {
|
|
201
200
|
createElement({ url: '/base-element.js' }, ({ perPage }) => {
|
|
202
201
|
const { count, actions } = useReducer({
|
|
203
202
|
initial: {
|
|
@@ -224,11 +223,11 @@ test('createElement with attrs and hooks', () => {
|
|
|
224
223
|
const Clazz = getElement('base-element');
|
|
225
224
|
const instance = new Clazz({ perPage: 5 });
|
|
226
225
|
const res = instance.render();
|
|
227
|
-
|
|
226
|
+
assert.deepStrictEqual(Clazz.observedAttributes, ['perpage']);
|
|
228
|
-
|
|
227
|
+
t.assert.snapshot(res);
|
|
229
228
|
});
|
|
230
229
|
|
|
231
|
-
test('createPage', () => {
|
|
230
|
+
test('createPage', (t) => {
|
|
232
231
|
const route = () => {
|
|
233
232
|
const langPart = this.config.lang === 'en' ? '' : `/${this.config.lang}`;
|
|
234
233
|
return `${langPart}`;
|
|
@@ -258,5 +257,5 @@ test('createPage', () => {
|
|
|
258
257
|
});
|
|
259
258
|
const scripts = '<script type="module"><script>';
|
|
260
259
|
const res = renderPage({ lang: 'en', props: { config: { title: '123' } }, headScript: scripts, bodyScript: scripts });
|
|
261
|
-
|
|
260
|
+
t.assert.snapshot(res);
|
|
262
261
|
});
|
index.test.js.snapshot
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
exports[`createElement styles are scoped and injected via SSR 1`] = `
|
|
2
|
+
"@scope (styled-element) {\\n\\n :scope {\\n display: flex;\\n }\\n .count {\\n color: magenta;\\n }\\n \\n}\\n"
|
|
3
|
+
`;
|
|
4
|
+
|
|
5
|
+
exports[`createElement with attrs and hooks 1`] = `
|
|
6
|
+
"\\n <div>\\n <div>\\n <span>perPage: 5</span> </div> </div> <span>Count: 3</span> <button>Set</button> "
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
exports[`createElement without attrs 1`] = `
|
|
10
|
+
" <div></div>"
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`createPage 1`] = `
|
|
14
|
+
"\\n <!DOCTYPE html>\\n <html lang=\\"en\\">\\n <head>\\n <meta charset=\\"utf-8\\" />\\n <meta http-equiv=\\"x-ua-compatible\\" content=\\"ie=edge\\" />\\n <meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5.0, shrink-to-fit=no\\">\\n <link rel=\\"sitemap\\" type=\\"application/xml\\" href=\\"/sitemap.xml\\" />\\n <link rel=\\"icon\\" type=\\"image/png\\" href=\\"/assets/icon.png\\" />\\n \\n <title>123</title> <meta name=\\"title\\" content=\\"123\\"/> <meta name=\\"description\\" content=\\"123\\"/>\\n <style id=\\"global\\">\\n *, ::before, ::after {\\n\\n box-sizing: border-box;\\n border-width: 0;\\n border-style: solid;\\n border-color: #e5e7eb;\\n\\n}\\nhr {\\n\\n height: 0;\\n color: inherit;\\n border-top-width: 1px;\\n\\n}\\nabbr[title] {\\n\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n\\n}\\nb, strong {\\n\\n font-weight: bolder;\\n\\n}\\ncode, kbd, samp, pre {\\n\\n font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;\\n font-size: 1em;\\n\\n}\\nsmall {\\n\\n font-size: 80%;\\n\\n}\\nsub, sup {\\n\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n\\n}\\nsub {\\n\\n bottom: -0.25em;\\n\\n}\\nsup {\\n\\n top: -0.5em;\\n\\n}\\ntable {\\n\\n text-indent: 0;\\n border-color: inherit;\\n border-collapse: collapse;\\n\\n}\\nbutton, input, optgroup, select, textarea {\\n\\n font-size: 100%;\\n margin: 0;\\n padding: 0;\\n line-height: inherit;\\n color: inherit;\\n\\n}\\nbutton, select {\\n\\n\\n}\\nbutton, [type='button'], [type='reset'], [type='submit'] {\\n\\n\\n}\\n::-moz-focus-inner {\\n\\n border-style: none;\\n padding: 0;\\n\\n}\\n:-moz-focusring {\\n\\n outline: 1px dotted ButtonText;\\n\\n}\\n:-moz-ui-invalid {\\n\\n box-shadow: none;\\n\\n}\\nlegend {\\n\\n padding: 0;\\n\\n}\\nprogress {\\n\\n vertical-align: baseline;\\n\\n}\\n::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\\n\\n height: auto;\\n\\n}\\n[type='search'] {\\n\\n -webkit-appearance: textfield;\\n outline-offset: -2px;\\n\\n}\\n::-webkit-search-decoration {\\n\\n -webkit-appearance: none;\\n\\n}\\n::-webkit-file-upload-button {\\n\\n -webkit-appearance: button;\\n font: inherit;\\n\\n}\\nsummary {\\n\\n display: list-item;\\n\\n}\\nblockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre {\\n\\n margin: 0;\\n\\n}\\nbutton {\\n\\n background-image: none;\\n\\n outline: 1px dotted, 5px auto -webkit-focus-ring-color;\\n\\n}\\nfieldset {\\n\\n margin: 0;\\n padding: 0;\\n\\n}\\nol, ul {\\n\\n list-style: none;\\n margin: 0;\\n padding: 0;\\n\\n}\\nimg {\\n\\n border-style: solid;\\n\\n}\\ntextarea {\\n\\n resize: vertical;\\n\\n}\\ninput::-moz-placeholder, textarea::-moz-placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\ninput:-ms-input-placeholder, textarea:-ms-input-placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\ninput::placeholder, textarea::placeholder {\\n\\n opacity: 1;\\n color: #9ca3af;\\n\\n}\\nbutton, [role='button'] {\\n\\n cursor: pointer;\\n\\n}\\nh1, h2, h3, h4, h5, h6 {\\n\\n font-size: inherit;\\n font-weight: inherit;\\n\\n}\\na {\\n\\n color: inherit;\\n text-decoration: inherit;\\n\\n}\\npre, code, kbd, samp {\\n\\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;\\n\\n}\\nimg, svg, video, canvas, audio, iframe, embed, object {\\n\\n display: block;\\n vertical-align: middle;\\n\\n}\\nimg, video {\\n\\n max-width: 100%;\\n height: auto;\\n\\n}\\nhtml {\\n\\n -moz-tab-size: 4;\\n -o-tab-size: 4;\\n tab-size: 4;\\n line-height: 1.5;\\n -webkit-text-size-adjust: 100%;\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\\n width: 100%;\\n height: 100%;\\n\\n}\\nbody {\\n\\n margin: 0px;\\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\\n line-height: 1.4;\\n background-color: white;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n flex-direction: column;\\n flex: 1 1 0%;\\n min-width: 320px;\\n min-height: 100vh;\\n font-weight: 400;\\n color: rgba(44, 62, 80, 1);\\n direction: ltr;\\n font-synthesis: none;\\n text-rendering: optimizeLegibility;\\n\\n}\\n\\n \\n </style>\\n <script type=\\"module\\"><script>\\n </head>\\n <body>\\n \\n <div>\\n <app-header></app-header> <main class=\\"flex flex-1 flex-col mt-20 items-center\\">\\n <h1 class=\\"text-5xl\\">123</h1> </main> </div>\\n <script>\\n window.props = {\\"config\\":{\\"title\\":\\"123\\"}};\\n </script>\\n <script type=\\"module\\"><script>\\n </body>\\n </html>\\n "
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
exports[`css tagged template 1`] = `
|
|
18
|
+
"\\n :scope {\\n display: flex;\\n }\\n button {\\n color: magenta;\\n }\\n "
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
exports[`render attribute keys 1`] = `
|
|
22
|
+
"\\n <div>\\n <app-counter name=\\"123\\" perPage=\\"1\\"></app-counter> </div>"
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
exports[`render attributes within quotes 1`] = `
|
|
26
|
+
"\\n <div>\\n <app-counter name=\\"123\\" class=\\"high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
exports[`render multi template 1`] = `
|
|
30
|
+
"\\n <div>\\n \\n <app-item meta=\\"{'index':1}\\">\\n <button>+</button> </app-item> <app-item meta=\\"{'index':2}\\">\\n <button>+</button> </app-item> </div>"
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
exports[`render single template 1`] = `
|
|
34
|
+
" <div>NoCountry false</div>"
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
exports[`render unsafeHTML 1`] = `
|
|
38
|
+
" <div><div><p class=\\"123\\">this is unsafe</p></div></div>"
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
exports[`renderHtml 1`] = `
|
|
42
|
+
"\\n <div>\\n <app-counter name=\\"123\\" class=\\"abc high\\" age=\\"1\\" details1=\\"{'name':'123','address':{'street':'1'}}\\" items=\\"[1,2,3]\\"></app-counter> </div>"
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
exports[`renderHtml escapes text and attribute values 1`] = `
|
|
46
|
+
"<div class=\\"a" onmouseover="alert(1)\\"><script>alert(1)</script></div>"
|
|
47
|
+
`;
|
lit-html.js
DELETED
|
@@ -1,1244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
4
|
-
* This code may only be used under the BSD style license found at
|
|
5
|
-
* http://polymer.github.io/LICENSE.txt
|
|
6
|
-
* The complete set of authors may be found at
|
|
7
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
8
|
-
* The complete set of contributors may be found at
|
|
9
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
10
|
-
* Code distributed by Google as part of the polymer project is also
|
|
11
|
-
* subject to an additional IP rights grant found at
|
|
12
|
-
* http://polymer.github.io/PATENTS.txt
|
|
13
|
-
*/
|
|
14
|
-
const directives = new WeakMap();
|
|
15
|
-
/**
|
|
16
|
-
* Brands a function as a directive factory function so that lit-html will call
|
|
17
|
-
* the function during template rendering, rather than passing as a value.
|
|
18
|
-
*
|
|
19
|
-
* A _directive_ is a function that takes a Part as an argument. It has the
|
|
20
|
-
* signature: `(part: Part) => void`.
|
|
21
|
-
*
|
|
22
|
-
* A directive _factory_ is a function that takes arguments for data and
|
|
23
|
-
* configuration and returns a directive. Users of directive usually refer to
|
|
24
|
-
* the directive factory as the directive. For example, "The repeat directive".
|
|
25
|
-
*
|
|
26
|
-
* Usually a template author will invoke a directive factory in their template
|
|
27
|
-
* with relevant arguments, which will then return a directive function.
|
|
28
|
-
*
|
|
29
|
-
* Here's an example of using the `repeat()` directive factory that takes an
|
|
30
|
-
* array and a function to render an item:
|
|
31
|
-
*
|
|
32
|
-
* ```js
|
|
33
|
-
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>`
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* When `repeat` is invoked, it returns a directive function that closes over
|
|
37
|
-
* `items` and the template function. When the outer template is rendered, the
|
|
38
|
-
* return directive function is called with the Part for the expression.
|
|
39
|
-
* `repeat` then performs it's custom logic to render multiple items.
|
|
40
|
-
*
|
|
41
|
-
* @param f The directive factory function. Must be a function that returns a
|
|
42
|
-
* function of the signature `(part: Part) => void`. The returned function will
|
|
43
|
-
* be called with the part object.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
*
|
|
47
|
-
* import {directive, html} from 'lit-html';
|
|
48
|
-
*
|
|
49
|
-
* const immutable = directive((v) => (part) => {
|
|
50
|
-
* if (part.value !== v) {
|
|
51
|
-
* part.setValue(v)
|
|
52
|
-
* }
|
|
53
|
-
* });
|
|
54
|
-
*/
|
|
55
|
-
const directive =
|
|
56
|
-
(f) =>
|
|
57
|
-
(...args) => {
|
|
58
|
-
const d = f(...args);
|
|
59
|
-
directives.set(d, true);
|
|
60
|
-
return d;
|
|
61
|
-
};
|
|
62
|
-
const isDirective = (o) => {
|
|
63
|
-
return typeof o === 'function' && directives.has(o);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @license
|
|
68
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
69
|
-
* This code may only be used under the BSD style license found at
|
|
70
|
-
* http://polymer.github.io/LICENSE.txt
|
|
71
|
-
* The complete set of authors may be found at
|
|
72
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
73
|
-
* The complete set of contributors may be found at
|
|
74
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
75
|
-
* Code distributed by Google as part of the polymer project is also
|
|
76
|
-
* subject to an additional IP rights grant found at
|
|
77
|
-
* http://polymer.github.io/PATENTS.txt
|
|
78
|
-
*/
|
|
79
|
-
/**
|
|
80
|
-
* True if the custom elements polyfill is in use.
|
|
81
|
-
*/
|
|
82
|
-
const isCEPolyfill = typeof window !== 'undefined' && window.customElements != null && window.customElements.polyfillWrapFlushCallback !== undefined;
|
|
83
|
-
/**
|
|
84
|
-
* Reparents nodes, starting from `start` (inclusive) to `end` (exclusive),
|
|
85
|
-
* into another container (could be the same container), before `before`. If
|
|
86
|
-
* `before` is null, it appends the nodes to the container.
|
|
87
|
-
*/
|
|
88
|
-
const reparentNodes = (container, start, end = null, before = null) => {
|
|
89
|
-
while (start !== end) {
|
|
90
|
-
const n = start.nextSibling;
|
|
91
|
-
container.insertBefore(start, before);
|
|
92
|
-
start = n;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
/**
|
|
96
|
-
* Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from
|
|
97
|
-
* `container`.
|
|
98
|
-
*/
|
|
99
|
-
const removeNodes = (container, start, end = null) => {
|
|
100
|
-
while (start !== end) {
|
|
101
|
-
const n = start.nextSibling;
|
|
102
|
-
container.removeChild(start);
|
|
103
|
-
start = n;
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* @license
|
|
109
|
-
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
|
110
|
-
* This code may only be used under the BSD style license found at
|
|
111
|
-
* http://polymer.github.io/LICENSE.txt
|
|
112
|
-
* The complete set of authors may be found at
|
|
113
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
114
|
-
* The complete set of contributors may be found at
|
|
115
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
116
|
-
* Code distributed by Google as part of the polymer project is also
|
|
117
|
-
* subject to an additional IP rights grant found at
|
|
118
|
-
* http://polymer.github.io/PATENTS.txt
|
|
119
|
-
*/
|
|
120
|
-
/**
|
|
121
|
-
* A sentinel value that signals that a value was handled by a directive and
|
|
122
|
-
* should not be written to the DOM.
|
|
123
|
-
*/
|
|
124
|
-
const noChange = {};
|
|
125
|
-
/**
|
|
126
|
-
* A sentinel value that signals a NodePart to fully clear its content.
|
|
127
|
-
*/
|
|
128
|
-
const nothing = {};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* @license
|
|
132
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
133
|
-
* This code may only be used under the BSD style license found at
|
|
134
|
-
* http://polymer.github.io/LICENSE.txt
|
|
135
|
-
* The complete set of authors may be found at
|
|
136
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
137
|
-
* The complete set of contributors may be found at
|
|
138
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
139
|
-
* Code distributed by Google as part of the polymer project is also
|
|
140
|
-
* subject to an additional IP rights grant found at
|
|
141
|
-
* http://polymer.github.io/PATENTS.txt
|
|
142
|
-
*/
|
|
143
|
-
/**
|
|
144
|
-
* An expression marker with embedded unique key to avoid collision with
|
|
145
|
-
* possible text in templates.
|
|
146
|
-
*/
|
|
147
|
-
const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
|
|
148
|
-
/**
|
|
149
|
-
* An expression marker used text-positions, multi-binding attributes, and
|
|
150
|
-
* attributes with markup-like text values.
|
|
151
|
-
*/
|
|
152
|
-
const nodeMarker = `<!--${marker}-->`;
|
|
153
|
-
const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
|
|
154
|
-
/**
|
|
155
|
-
* Suffix appended to all bound attribute names.
|
|
156
|
-
*/
|
|
157
|
-
const boundAttributeSuffix = '$lit$';
|
|
158
|
-
/**
|
|
159
|
-
* An updatable Template that tracks the location of dynamic parts.
|
|
160
|
-
*/
|
|
161
|
-
class Template {
|
|
162
|
-
constructor(result, element) {
|
|
163
|
-
this.parts = [];
|
|
164
|
-
this.element = element;
|
|
165
|
-
const nodesToRemove = [];
|
|
166
|
-
const stack = [];
|
|
167
|
-
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
|
|
168
|
-
const walker = document.createTreeWalker(element.content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
|
|
169
|
-
// Keeps track of the last index associated with a part. We try to delete
|
|
170
|
-
// unnecessary nodes, but we never want to associate two different parts
|
|
171
|
-
// to the same index. They must have a constant node between.
|
|
172
|
-
let lastPartIndex = 0;
|
|
173
|
-
let index = -1;
|
|
174
|
-
let partIndex = 0;
|
|
175
|
-
const {
|
|
176
|
-
strings,
|
|
177
|
-
values: { length },
|
|
178
|
-
} = result;
|
|
179
|
-
while (partIndex < length) {
|
|
180
|
-
const node = walker.nextNode();
|
|
181
|
-
if (node === null) {
|
|
182
|
-
// We've exhausted the content inside a nested template element.
|
|
183
|
-
// Because we still have parts (the outer for-loop), we know:
|
|
184
|
-
// - There is a template in the stack
|
|
185
|
-
// - The walker will find a nextNode outside the template
|
|
186
|
-
walker.currentNode = stack.pop();
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
index++;
|
|
190
|
-
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
|
191
|
-
if (node.hasAttributes()) {
|
|
192
|
-
const attributes = node.attributes;
|
|
193
|
-
const { length } = attributes;
|
|
194
|
-
// Per
|
|
195
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,
|
|
196
|
-
// attributes are not guaranteed to be returned in document order.
|
|
197
|
-
// In particular, Edge/IE can return them out of order, so we cannot
|
|
198
|
-
// assume a correspondence between part index and attribute index.
|
|
199
|
-
let count = 0;
|
|
200
|
-
for (let i = 0; i < length; i++) {
|
|
201
|
-
if (endsWith(attributes[i].name, boundAttributeSuffix)) {
|
|
202
|
-
count++;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
while (count-- > 0) {
|
|
206
|
-
// Get the template literal section leading up to the first
|
|
207
|
-
// expression in this attribute
|
|
208
|
-
const stringForPart = strings[partIndex];
|
|
209
|
-
// Find the attribute name
|
|
210
|
-
const name = lastAttributeNameRegex.exec(stringForPart)[2];
|
|
211
|
-
// Find the corresponding attribute
|
|
212
|
-
// All bound attributes have had a suffix added in
|
|
213
|
-
// TemplateResult#getHTML to opt out of special attribute
|
|
214
|
-
// handling. To look up the attribute value we also need to add
|
|
215
|
-
// the suffix.
|
|
216
|
-
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix;
|
|
217
|
-
const attributeValue = node.getAttribute(attributeLookupName);
|
|
218
|
-
node.removeAttribute(attributeLookupName);
|
|
219
|
-
const statics = attributeValue.split(markerRegex);
|
|
220
|
-
this.parts.push({ type: 'attribute', index, name, strings: statics });
|
|
221
|
-
partIndex += statics.length - 1;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (node.tagName === 'TEMPLATE') {
|
|
225
|
-
stack.push(node);
|
|
226
|
-
walker.currentNode = node.content;
|
|
227
|
-
}
|
|
228
|
-
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
|
|
229
|
-
const data = node.data;
|
|
230
|
-
if (data.indexOf(marker) >= 0) {
|
|
231
|
-
const parent = node.parentNode;
|
|
232
|
-
const strings = data.split(markerRegex);
|
|
233
|
-
const lastIndex = strings.length - 1;
|
|
234
|
-
// Generate a new text node for each literal section
|
|
235
|
-
// These nodes are also used as the markers for node parts
|
|
236
|
-
for (let i = 0; i < lastIndex; i++) {
|
|
237
|
-
let insert;
|
|
238
|
-
let s = strings[i];
|
|
239
|
-
if (s === '') {
|
|
240
|
-
insert = createMarker();
|
|
241
|
-
} else {
|
|
242
|
-
const match = lastAttributeNameRegex.exec(s);
|
|
243
|
-
if (match !== null && endsWith(match[2], boundAttributeSuffix)) {
|
|
244
|
-
s = s.slice(0, match.index) + match[1] + match[2].slice(0, -boundAttributeSuffix.length) + match[3];
|
|
245
|
-
}
|
|
246
|
-
insert = document.createTextNode(s);
|
|
247
|
-
}
|
|
248
|
-
parent.insertBefore(insert, node);
|
|
249
|
-
this.parts.push({ type: 'node', index: ++index });
|
|
250
|
-
}
|
|
251
|
-
// If there's no text, we must insert a comment to mark our place.
|
|
252
|
-
// Else, we can trust it will stick around after cloning.
|
|
253
|
-
if (strings[lastIndex] === '') {
|
|
254
|
-
parent.insertBefore(createMarker(), node);
|
|
255
|
-
nodesToRemove.push(node);
|
|
256
|
-
} else {
|
|
257
|
-
node.data = strings[lastIndex];
|
|
258
|
-
}
|
|
259
|
-
// We have a part for each match found
|
|
260
|
-
partIndex += lastIndex;
|
|
261
|
-
}
|
|
262
|
-
} else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
|
|
263
|
-
if (node.data === marker) {
|
|
264
|
-
const parent = node.parentNode;
|
|
265
|
-
// Add a new marker node to be the startNode of the Part if any of
|
|
266
|
-
// the following are true:
|
|
267
|
-
// * We don't have a previousSibling
|
|
268
|
-
// * The previousSibling is already the start of a previous part
|
|
269
|
-
if (node.previousSibling === null || index === lastPartIndex) {
|
|
270
|
-
index++;
|
|
271
|
-
parent.insertBefore(createMarker(), node);
|
|
272
|
-
}
|
|
273
|
-
lastPartIndex = index;
|
|
274
|
-
this.parts.push({ type: 'node', index });
|
|
275
|
-
// If we don't have a nextSibling, keep this node so we have an end.
|
|
276
|
-
// Else, we can remove it to save future costs.
|
|
277
|
-
if (node.nextSibling === null) {
|
|
278
|
-
node.data = '';
|
|
279
|
-
} else {
|
|
280
|
-
nodesToRemove.push(node);
|
|
281
|
-
index--;
|
|
282
|
-
}
|
|
283
|
-
partIndex++;
|
|
284
|
-
} else {
|
|
285
|
-
let i = -1;
|
|
286
|
-
while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
|
|
287
|
-
// Comment node has a binding marker inside, make an inactive part
|
|
288
|
-
// The binding won't work, but subsequent bindings will
|
|
289
|
-
// TODO (justinfagnani): consider whether it's even worth it to
|
|
290
|
-
// make bindings in comments work
|
|
291
|
-
this.parts.push({ type: 'node', index: -1 });
|
|
292
|
-
partIndex++;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// Remove text binding nodes after the walk to not disturb the TreeWalker
|
|
298
|
-
for (const n of nodesToRemove) {
|
|
299
|
-
n.parentNode.removeChild(n);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
const endsWith = (str, suffix) => {
|
|
304
|
-
const index = str.length - suffix.length;
|
|
305
|
-
return index >= 0 && str.slice(index) === suffix;
|
|
306
|
-
};
|
|
307
|
-
const isTemplatePartActive = (part) => part.index !== -1;
|
|
308
|
-
// Allows `document.createComment('')` to be renamed for a
|
|
309
|
-
// small manual size-savings.
|
|
310
|
-
const createMarker = () => document.createComment('');
|
|
311
|
-
/**
|
|
312
|
-
* This regex extracts the attribute name preceding an attribute-position
|
|
313
|
-
* expression. It does this by matching the syntax allowed for attributes
|
|
314
|
-
* against the string literal directly preceding the expression, assuming that
|
|
315
|
-
* the expression is in an attribute-value position.
|
|
316
|
-
*
|
|
317
|
-
* See attributes in the HTML spec:
|
|
318
|
-
* https://www.w3.org/TR/html5/syntax.html#elements-attributes
|
|
319
|
-
*
|
|
320
|
-
* " \x09\x0a\x0c\x0d" are HTML space characters:
|
|
321
|
-
* https://www.w3.org/TR/html5/infrastructure.html#space-characters
|
|
322
|
-
*
|
|
323
|
-
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every
|
|
324
|
-
* space character except " ".
|
|
325
|
-
*
|
|
326
|
-
* So an attribute is:
|
|
327
|
-
* * The name: any character except a control character, space character, ('),
|
|
328
|
-
* ("), ">", "=", or "/"
|
|
329
|
-
* * Followed by zero or more space characters
|
|
330
|
-
* * Followed by "="
|
|
331
|
-
* * Followed by zero or more space characters
|
|
332
|
-
* * Followed by:
|
|
333
|
-
* * Any character except space, ('), ("), "<", ">", "=", (`), or
|
|
334
|
-
* * (") then any non-("), or
|
|
335
|
-
* * (') then any non-(')
|
|
336
|
-
*/
|
|
337
|
-
const lastAttributeNameRegex =
|
|
338
|
-
// eslint-disable-next-line no-control-regex
|
|
339
|
-
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* @license
|
|
343
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
344
|
-
* This code may only be used under the BSD style license found at
|
|
345
|
-
* http://polymer.github.io/LICENSE.txt
|
|
346
|
-
* The complete set of authors may be found at
|
|
347
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
348
|
-
* The complete set of contributors may be found at
|
|
349
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
350
|
-
* Code distributed by Google as part of the polymer project is also
|
|
351
|
-
* subject to an additional IP rights grant found at
|
|
352
|
-
* http://polymer.github.io/PATENTS.txt
|
|
353
|
-
*/
|
|
354
|
-
/**
|
|
355
|
-
* An instance of a `Template` that can be attached to the DOM and updated
|
|
356
|
-
* with new values.
|
|
357
|
-
*/
|
|
358
|
-
class TemplateInstance {
|
|
359
|
-
constructor(template, processor, options) {
|
|
360
|
-
this.__parts = [];
|
|
361
|
-
this.template = template;
|
|
362
|
-
this.processor = processor;
|
|
363
|
-
this.options = options;
|
|
364
|
-
}
|
|
365
|
-
update(values) {
|
|
366
|
-
let i = 0;
|
|
367
|
-
for (const part of this.__parts) {
|
|
368
|
-
if (part !== undefined) {
|
|
369
|
-
part.setValue(values[i]);
|
|
370
|
-
}
|
|
371
|
-
i++;
|
|
372
|
-
}
|
|
373
|
-
for (const part of this.__parts) {
|
|
374
|
-
if (part !== undefined) {
|
|
375
|
-
part.commit();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
_clone() {
|
|
380
|
-
// There are a number of steps in the lifecycle of a template instance's
|
|
381
|
-
// DOM fragment:
|
|
382
|
-
// 1. Clone - create the instance fragment
|
|
383
|
-
// 2. Adopt - adopt into the main document
|
|
384
|
-
// 3. Process - find part markers and create parts
|
|
385
|
-
// 4. Upgrade - upgrade custom elements
|
|
386
|
-
// 5. Update - set node, attribute, property, etc., values
|
|
387
|
-
// 6. Connect - connect to the document. Optional and outside of this
|
|
388
|
-
// method.
|
|
389
|
-
//
|
|
390
|
-
// We have a few constraints on the ordering of these steps:
|
|
391
|
-
// * We need to upgrade before updating, so that property values will pass
|
|
392
|
-
// through any property setters.
|
|
393
|
-
// * We would like to process before upgrading so that we're sure that the
|
|
394
|
-
// cloned fragment is inert and not disturbed by self-modifying DOM.
|
|
395
|
-
// * We want custom elements to upgrade even in disconnected fragments.
|
|
396
|
-
//
|
|
397
|
-
// Given these constraints, with full custom elements support we would
|
|
398
|
-
// prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect
|
|
399
|
-
//
|
|
400
|
-
// But Safari does not implement CustomElementRegistry#upgrade, so we
|
|
401
|
-
// can not implement that order and still have upgrade-before-update and
|
|
402
|
-
// upgrade disconnected fragments. So we instead sacrifice the
|
|
403
|
-
// process-before-upgrade constraint, since in Custom Elements v1 elements
|
|
404
|
-
// must not modify their light DOM in the constructor. We still have issues
|
|
405
|
-
// when co-existing with CEv0 elements like Polymer 1, and with polyfills
|
|
406
|
-
// that don't strictly adhere to the no-modification rule because shadow
|
|
407
|
-
// DOM, which may be created in the constructor, is emulated by being placed
|
|
408
|
-
// in the light DOM.
|
|
409
|
-
//
|
|
410
|
-
// The resulting order is on native is: Clone, Adopt, Upgrade, Process,
|
|
411
|
-
// Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade
|
|
412
|
-
// in one step.
|
|
413
|
-
//
|
|
414
|
-
// The Custom Elements v1 polyfill supports upgrade(), so the order when
|
|
415
|
-
// polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update,
|
|
416
|
-
// Connect.
|
|
417
|
-
const fragment = isCEPolyfill ? this.template.element.content.cloneNode(true) : document.importNode(this.template.element.content, true);
|
|
418
|
-
const stack = [];
|
|
419
|
-
const parts = this.template.parts;
|
|
420
|
-
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
|
|
421
|
-
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
|
|
422
|
-
let partIndex = 0;
|
|
423
|
-
let nodeIndex = 0;
|
|
424
|
-
let part;
|
|
425
|
-
let node = walker.nextNode();
|
|
426
|
-
// Loop through all the nodes and parts of a template
|
|
427
|
-
while (partIndex < parts.length) {
|
|
428
|
-
part = parts[partIndex];
|
|
429
|
-
if (!isTemplatePartActive(part)) {
|
|
430
|
-
this.__parts.push(undefined);
|
|
431
|
-
partIndex++;
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
// Progress the tree walker until we find our next part's node.
|
|
435
|
-
// Note that multiple parts may share the same node (attribute parts
|
|
436
|
-
// on a single element), so this loop may not run at all.
|
|
437
|
-
while (nodeIndex < part.index) {
|
|
438
|
-
nodeIndex++;
|
|
439
|
-
if (node.nodeName === 'TEMPLATE') {
|
|
440
|
-
stack.push(node);
|
|
441
|
-
walker.currentNode = node.content;
|
|
442
|
-
}
|
|
443
|
-
if ((node = walker.nextNode()) === null) {
|
|
444
|
-
// We've exhausted the content inside a nested template element.
|
|
445
|
-
// Because we still have parts (the outer for-loop), we know:
|
|
446
|
-
// - There is a template in the stack
|
|
447
|
-
// - The walker will find a nextNode outside the template
|
|
448
|
-
walker.currentNode = stack.pop();
|
|
449
|
-
node = walker.nextNode();
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
// We've arrived at our part's node.
|
|
453
|
-
if (part.type === 'node') {
|
|
454
|
-
const part = this.processor.handleTextExpression(this.options);
|
|
455
|
-
part.insertAfterNode(node.previousSibling);
|
|
456
|
-
this.__parts.push(part);
|
|
457
|
-
} else {
|
|
458
|
-
this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options));
|
|
459
|
-
}
|
|
460
|
-
partIndex++;
|
|
461
|
-
}
|
|
462
|
-
if (isCEPolyfill) {
|
|
463
|
-
document.adoptNode(fragment);
|
|
464
|
-
customElements.upgrade(fragment);
|
|
465
|
-
}
|
|
466
|
-
return fragment;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* @license
|
|
472
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
473
|
-
* This code may only be used under the BSD style license found at
|
|
474
|
-
* http://polymer.github.io/LICENSE.txt
|
|
475
|
-
* The complete set of authors may be found at
|
|
476
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
477
|
-
* The complete set of contributors may be found at
|
|
478
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
479
|
-
* Code distributed by Google as part of the polymer project is also
|
|
480
|
-
* subject to an additional IP rights grant found at
|
|
481
|
-
* http://polymer.github.io/PATENTS.txt
|
|
482
|
-
*/
|
|
483
|
-
/**
|
|
484
|
-
* Our TrustedTypePolicy for HTML which is declared using the html template
|
|
485
|
-
* tag function.
|
|
486
|
-
*
|
|
487
|
-
* That HTML is a developer-authored constant, and is parsed with innerHTML
|
|
488
|
-
* before any untrusted expressions have been mixed in. Therefor it is
|
|
489
|
-
* considered safe by construction.
|
|
490
|
-
*/
|
|
491
|
-
const policy = typeof window !== 'undefined' && window.trustedTypes && trustedTypes.createPolicy('lit-html', { createHTML: (s) => s });
|
|
492
|
-
const commentMarker = ` ${marker} `;
|
|
493
|
-
/**
|
|
494
|
-
* The return type of `html`, which holds a Template and the values from
|
|
495
|
-
* interpolated expressions.
|
|
496
|
-
*/
|
|
497
|
-
class TemplateResult {
|
|
498
|
-
constructor(strings, values, type, processor) {
|
|
499
|
-
this.strings = strings;
|
|
500
|
-
this.values = values;
|
|
501
|
-
this.type = type;
|
|
502
|
-
this.processor = processor;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Returns a string of HTML used to create a `<template>` element.
|
|
506
|
-
*/
|
|
507
|
-
getHTML() {
|
|
508
|
-
const l = this.strings.length - 1;
|
|
509
|
-
let html = '';
|
|
510
|
-
let isCommentBinding = false;
|
|
511
|
-
for (let i = 0; i < l; i++) {
|
|
512
|
-
const s = this.strings[i];
|
|
513
|
-
// For each binding we want to determine the kind of marker to insert
|
|
514
|
-
// into the template source before it's parsed by the browser's HTML
|
|
515
|
-
// parser. The marker type is based on whether the expression is in an
|
|
516
|
-
// attribute, text, or comment position.
|
|
517
|
-
// * For node-position bindings we insert a comment with the marker
|
|
518
|
-
// sentinel as its text content, like <!--{{lit-guid}}-->.
|
|
519
|
-
// * For attribute bindings we insert just the marker sentinel for the
|
|
520
|
-
// first binding, so that we support unquoted attribute bindings.
|
|
521
|
-
// Subsequent bindings can use a comment marker because multi-binding
|
|
522
|
-
// attributes must be quoted.
|
|
523
|
-
// * For comment bindings we insert just the marker sentinel so we don't
|
|
524
|
-
// close the comment.
|
|
525
|
-
//
|
|
526
|
-
// The following code scans the template source, but is *not* an HTML
|
|
527
|
-
// parser. We don't need to track the tree structure of the HTML, only
|
|
528
|
-
// whether a binding is inside a comment, and if not, if it appears to be
|
|
529
|
-
// the first binding in an attribute.
|
|
530
|
-
const commentOpen = s.lastIndexOf('<!--');
|
|
531
|
-
// We're in comment position if we have a comment open with no following
|
|
532
|
-
// comment close. Because <-- can appear in an attribute value there can
|
|
533
|
-
// be false positives.
|
|
534
|
-
isCommentBinding = (commentOpen > -1 || isCommentBinding) && s.indexOf('-->', commentOpen + 1) === -1;
|
|
535
|
-
// Check to see if we have an attribute-like sequence preceding the
|
|
536
|
-
// expression. This can match "name=value" like structures in text,
|
|
537
|
-
// comments, and attribute values, so there can be false-positives.
|
|
538
|
-
const attributeMatch = lastAttributeNameRegex.exec(s);
|
|
539
|
-
if (attributeMatch === null) {
|
|
540
|
-
// We're only in this branch if we don't have a attribute-like
|
|
541
|
-
// preceding sequence. For comments, this guards against unusual
|
|
542
|
-
// attribute values like <div foo="<!--${'bar'}">. Cases like
|
|
543
|
-
// <!-- foo=${'bar'}--> are handled correctly in the attribute branch
|
|
544
|
-
// below.
|
|
545
|
-
html += s + (isCommentBinding ? commentMarker : nodeMarker);
|
|
546
|
-
} else {
|
|
547
|
-
// For attributes we use just a marker sentinel, and also append a
|
|
548
|
-
// $lit$ suffix to the name to opt-out of attribute-specific parsing
|
|
549
|
-
// that IE and Edge do for style and certain SVG attributes.
|
|
550
|
-
html += s.substr(0, attributeMatch.index) + attributeMatch[1] + attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] + marker;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
html += this.strings[l];
|
|
554
|
-
return html;
|
|
555
|
-
}
|
|
556
|
-
getTemplateElement() {
|
|
557
|
-
const template = document.createElement('template');
|
|
558
|
-
let value = this.getHTML();
|
|
559
|
-
if (policy !== undefined) {
|
|
560
|
-
// this is secure because `this.strings` is a TemplateStringsArray.
|
|
561
|
-
// TODO: validate this when
|
|
562
|
-
// https://github.com/tc39/proposal-array-is-template-object is
|
|
563
|
-
// implemented.
|
|
564
|
-
value = policy.createHTML(value);
|
|
565
|
-
}
|
|
566
|
-
template.innerHTML = value;
|
|
567
|
-
return template;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* A TemplateResult for SVG fragments.
|
|
572
|
-
*
|
|
573
|
-
* This class wraps HTML in an `<svg>` tag in order to parse its contents in the
|
|
574
|
-
* SVG namespace, then modifies the template to remove the `<svg>` tag so that
|
|
575
|
-
* clones only container the original fragment.
|
|
576
|
-
*/
|
|
577
|
-
class SVGTemplateResult extends TemplateResult {
|
|
578
|
-
getHTML() {
|
|
579
|
-
return `<svg>${super.getHTML()}</svg>`;
|
|
580
|
-
}
|
|
581
|
-
getTemplateElement() {
|
|
582
|
-
const template = super.getTemplateElement();
|
|
583
|
-
const content = template.content;
|
|
584
|
-
const svgElement = content.firstChild;
|
|
585
|
-
content.removeChild(svgElement);
|
|
586
|
-
reparentNodes(content, svgElement.firstChild);
|
|
587
|
-
return template;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* @license
|
|
593
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
594
|
-
* This code may only be used under the BSD style license found at
|
|
595
|
-
* http://polymer.github.io/LICENSE.txt
|
|
596
|
-
* The complete set of authors may be found at
|
|
597
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
598
|
-
* The complete set of contributors may be found at
|
|
599
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
600
|
-
* Code distributed by Google as part of the polymer project is also
|
|
601
|
-
* subject to an additional IP rights grant found at
|
|
602
|
-
* http://polymer.github.io/PATENTS.txt
|
|
603
|
-
*/
|
|
604
|
-
const isPrimitive = (value) => {
|
|
605
|
-
return value === null || !(typeof value === 'object' || typeof value === 'function');
|
|
606
|
-
};
|
|
607
|
-
const isIterable = (value) => {
|
|
608
|
-
return (
|
|
609
|
-
Array.isArray(value) ||
|
|
610
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
611
|
-
!!(value && value[Symbol.iterator])
|
|
612
|
-
);
|
|
613
|
-
};
|
|
614
|
-
/**
|
|
615
|
-
* Writes attribute values to the DOM for a group of AttributeParts bound to a
|
|
616
|
-
* single attribute. The value is only set once even if there are multiple parts
|
|
617
|
-
* for an attribute.
|
|
618
|
-
*/
|
|
619
|
-
class AttributeCommitter {
|
|
620
|
-
constructor(element, name, strings) {
|
|
621
|
-
this.dirty = true;
|
|
622
|
-
this.element = element;
|
|
623
|
-
this.name = name;
|
|
624
|
-
this.strings = strings;
|
|
625
|
-
this.parts = [];
|
|
626
|
-
for (let i = 0; i < strings.length - 1; i++) {
|
|
627
|
-
this.parts[i] = this._createPart();
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* Creates a single part. Override this to create a differnt type of part.
|
|
632
|
-
*/
|
|
633
|
-
_createPart() {
|
|
634
|
-
return new AttributePart(this);
|
|
635
|
-
}
|
|
636
|
-
_getValue() {
|
|
637
|
-
const strings = this.strings;
|
|
638
|
-
const l = strings.length - 1;
|
|
639
|
-
const parts = this.parts;
|
|
640
|
-
// If we're assigning an attribute via syntax like:
|
|
641
|
-
// attr="${foo}" or attr=${foo}
|
|
642
|
-
// but not
|
|
643
|
-
// attr="${foo} ${bar}" or attr="${foo} baz"
|
|
644
|
-
// then we don't want to coerce the attribute value into one long
|
|
645
|
-
// string. Instead we want to just return the value itself directly,
|
|
646
|
-
// so that sanitizeDOMValue can get the actual value rather than
|
|
647
|
-
// String(value)
|
|
648
|
-
// The exception is if v is an array, in which case we do want to smash
|
|
649
|
-
// it together into a string without calling String() on the array.
|
|
650
|
-
//
|
|
651
|
-
// This also allows trusted values (when using TrustedTypes) being
|
|
652
|
-
// assigned to DOM sinks without being stringified in the process.
|
|
653
|
-
if (l === 1 && strings[0] === '' && strings[1] === '') {
|
|
654
|
-
const v = parts[0].value;
|
|
655
|
-
if (typeof v === 'symbol') {
|
|
656
|
-
return String(v);
|
|
657
|
-
}
|
|
658
|
-
if (typeof v === 'string' || !isIterable(v)) {
|
|
659
|
-
return v;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
let text = '';
|
|
663
|
-
for (let i = 0; i < l; i++) {
|
|
664
|
-
text += strings[i];
|
|
665
|
-
const part = parts[i];
|
|
666
|
-
if (part !== undefined) {
|
|
667
|
-
const v = part.value;
|
|
668
|
-
if (isPrimitive(v) || !isIterable(v)) {
|
|
669
|
-
text += typeof v === 'string' ? v : String(v);
|
|
670
|
-
} else {
|
|
671
|
-
for (const t of v) {
|
|
672
|
-
text += typeof t === 'string' ? t : String(t);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
text += strings[l];
|
|
678
|
-
return text;
|
|
679
|
-
}
|
|
680
|
-
commit() {
|
|
681
|
-
if (this.dirty) {
|
|
682
|
-
this.dirty = false;
|
|
683
|
-
this.element.setAttribute(this.name, this._getValue());
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* A Part that controls all or part of an attribute value.
|
|
689
|
-
*/
|
|
690
|
-
class AttributePart {
|
|
691
|
-
constructor(committer) {
|
|
692
|
-
this.value = undefined;
|
|
693
|
-
this.committer = committer;
|
|
694
|
-
}
|
|
695
|
-
setValue(value) {
|
|
696
|
-
if (value !== noChange && (!isPrimitive(value) || value !== this.value)) {
|
|
697
|
-
if (typeof value === 'object') {
|
|
698
|
-
this.value = JSON.stringify(value).replace(/"/g, `'`);
|
|
699
|
-
} else {
|
|
700
|
-
this.value = value;
|
|
701
|
-
}
|
|
702
|
-
// If the value is a not a directive, dirty the committer so that it'll
|
|
703
|
-
// call setAttribute. If the value is a directive, it'll dirty the
|
|
704
|
-
// committer if it calls setValue().
|
|
705
|
-
if (!isDirective(value)) {
|
|
706
|
-
this.committer.dirty = true;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
commit() {
|
|
711
|
-
while (isDirective(this.value)) {
|
|
712
|
-
const directive = this.value;
|
|
713
|
-
this.value = noChange;
|
|
714
|
-
directive(this);
|
|
715
|
-
}
|
|
716
|
-
if (this.value === noChange) {
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
this.committer.commit();
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* A Part that controls a location within a Node tree. Like a Range, NodePart
|
|
724
|
-
* has start and end locations and can set and update the Nodes between those
|
|
725
|
-
* locations.
|
|
726
|
-
*
|
|
727
|
-
* NodeParts support several value types: primitives, Nodes, TemplateResults,
|
|
728
|
-
* as well as arrays and iterables of those types.
|
|
729
|
-
*/
|
|
730
|
-
class NodePart {
|
|
731
|
-
constructor(options) {
|
|
732
|
-
this.value = undefined;
|
|
733
|
-
this.__pendingValue = undefined;
|
|
734
|
-
this.options = options;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Appends this part into a container.
|
|
738
|
-
*
|
|
739
|
-
* This part must be empty, as its contents are not automatically moved.
|
|
740
|
-
*/
|
|
741
|
-
appendInto(container) {
|
|
742
|
-
this.startNode = container.appendChild(createMarker());
|
|
743
|
-
this.endNode = container.appendChild(createMarker());
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Inserts this part after the `ref` node (between `ref` and `ref`'s next
|
|
747
|
-
* sibling). Both `ref` and its next sibling must be static, unchanging nodes
|
|
748
|
-
* such as those that appear in a literal section of a template.
|
|
749
|
-
*
|
|
750
|
-
* This part must be empty, as its contents are not automatically moved.
|
|
751
|
-
*/
|
|
752
|
-
insertAfterNode(ref) {
|
|
753
|
-
this.startNode = ref;
|
|
754
|
-
this.endNode = ref.nextSibling;
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Appends this part into a parent part.
|
|
758
|
-
*
|
|
759
|
-
* This part must be empty, as its contents are not automatically moved.
|
|
760
|
-
*/
|
|
761
|
-
appendIntoPart(part) {
|
|
762
|
-
part.__insert((this.startNode = createMarker()));
|
|
763
|
-
part.__insert((this.endNode = createMarker()));
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Inserts this part after the `ref` part.
|
|
767
|
-
*
|
|
768
|
-
* This part must be empty, as its contents are not automatically moved.
|
|
769
|
-
*/
|
|
770
|
-
insertAfterPart(ref) {
|
|
771
|
-
ref.__insert((this.startNode = createMarker()));
|
|
772
|
-
this.endNode = ref.endNode;
|
|
773
|
-
ref.endNode = this.startNode;
|
|
774
|
-
}
|
|
775
|
-
setValue(value) {
|
|
776
|
-
this.__pendingValue = value;
|
|
777
|
-
}
|
|
778
|
-
commit() {
|
|
779
|
-
if (this.startNode.parentNode === null) {
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
while (isDirective(this.__pendingValue)) {
|
|
783
|
-
const directive = this.__pendingValue;
|
|
784
|
-
this.__pendingValue = noChange;
|
|
785
|
-
directive(this);
|
|
786
|
-
}
|
|
787
|
-
const value = this.__pendingValue;
|
|
788
|
-
if (value === noChange) {
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
if (isPrimitive(value)) {
|
|
792
|
-
if (value !== this.value) {
|
|
793
|
-
this.__commitText(value);
|
|
794
|
-
}
|
|
795
|
-
} else if (value instanceof TemplateResult) {
|
|
796
|
-
this.__commitTemplateResult(value);
|
|
797
|
-
} else if (value instanceof Node) {
|
|
798
|
-
this.__commitNode(value);
|
|
799
|
-
} else if (isIterable(value)) {
|
|
800
|
-
this.__commitIterable(value);
|
|
801
|
-
} else if (value === nothing) {
|
|
802
|
-
this.value = nothing;
|
|
803
|
-
this.clear();
|
|
804
|
-
} else {
|
|
805
|
-
// Fallback, will render the string representation
|
|
806
|
-
this.__commitText(value);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
__insert(node) {
|
|
810
|
-
this.endNode.parentNode.insertBefore(node, this.endNode);
|
|
811
|
-
}
|
|
812
|
-
__commitNode(value) {
|
|
813
|
-
if (this.value === value) {
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
this.clear();
|
|
817
|
-
this.__insert(value);
|
|
818
|
-
this.value = value;
|
|
819
|
-
}
|
|
820
|
-
__commitText(value) {
|
|
821
|
-
const node = this.startNode.nextSibling;
|
|
822
|
-
value = value == null ? '' : value;
|
|
823
|
-
// If `value` isn't already a string, we explicitly convert it here in case
|
|
824
|
-
// it can't be implicitly converted - i.e. it's a symbol.
|
|
825
|
-
const valueAsString = typeof value === 'string' ? value : String(value);
|
|
826
|
-
if (node === this.endNode.previousSibling && node.nodeType === 3 /* Node.TEXT_NODE */) {
|
|
827
|
-
// If we only have a single text node between the markers, we can just
|
|
828
|
-
// set its value, rather than replacing it.
|
|
829
|
-
// TODO(justinfagnani): Can we just check if this.value is primitive?
|
|
830
|
-
node.data = valueAsString;
|
|
831
|
-
} else {
|
|
832
|
-
this.__commitNode(document.createTextNode(valueAsString));
|
|
833
|
-
}
|
|
834
|
-
this.value = value;
|
|
835
|
-
}
|
|
836
|
-
__commitTemplateResult(value) {
|
|
837
|
-
const template = this.options.templateFactory(value);
|
|
838
|
-
if (this.value instanceof TemplateInstance && this.value.template === template) {
|
|
839
|
-
this.value.update(value.values);
|
|
840
|
-
} else {
|
|
841
|
-
// Make sure we propagate the template processor from the TemplateResult
|
|
842
|
-
// so that we use its syntax extension, etc. The template factory comes
|
|
843
|
-
// from the render function options so that it can control template
|
|
844
|
-
// caching and preprocessing.
|
|
845
|
-
const instance = new TemplateInstance(template, value.processor, this.options);
|
|
846
|
-
const fragment = instance._clone();
|
|
847
|
-
instance.update(value.values);
|
|
848
|
-
this.__commitNode(fragment);
|
|
849
|
-
this.value = instance;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
__commitIterable(value) {
|
|
853
|
-
// For an Iterable, we create a new InstancePart per item, then set its
|
|
854
|
-
// value to the item. This is a little bit of overhead for every item in
|
|
855
|
-
// an Iterable, but it lets us recurse easily and efficiently update Arrays
|
|
856
|
-
// of TemplateResults that will be commonly returned from expressions like:
|
|
857
|
-
// array.map((i) => html`${i}`), by reusing existing TemplateInstances.
|
|
858
|
-
// If _value is an array, then the previous render was of an
|
|
859
|
-
// iterable and _value will contain the NodeParts from the previous
|
|
860
|
-
// render. If _value is not an array, clear this part and make a new
|
|
861
|
-
// array for NodeParts.
|
|
862
|
-
if (!Array.isArray(this.value)) {
|
|
863
|
-
this.value = [];
|
|
864
|
-
this.clear();
|
|
865
|
-
}
|
|
866
|
-
// Lets us keep track of how many items we stamped so we can clear leftover
|
|
867
|
-
// items from a previous render
|
|
868
|
-
const itemParts = this.value;
|
|
869
|
-
let partIndex = 0;
|
|
870
|
-
let itemPart;
|
|
871
|
-
for (const item of value) {
|
|
872
|
-
// Try to reuse an existing part
|
|
873
|
-
itemPart = itemParts[partIndex];
|
|
874
|
-
// If no existing part, create a new one
|
|
875
|
-
if (itemPart === undefined) {
|
|
876
|
-
itemPart = new NodePart(this.options);
|
|
877
|
-
itemParts.push(itemPart);
|
|
878
|
-
if (partIndex === 0) {
|
|
879
|
-
itemPart.appendIntoPart(this);
|
|
880
|
-
} else {
|
|
881
|
-
itemPart.insertAfterPart(itemParts[partIndex - 1]);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
itemPart.setValue(item);
|
|
885
|
-
itemPart.commit();
|
|
886
|
-
partIndex++;
|
|
887
|
-
}
|
|
888
|
-
if (partIndex < itemParts.length) {
|
|
889
|
-
// Truncate the parts array so _value reflects the current state
|
|
890
|
-
itemParts.length = partIndex;
|
|
891
|
-
this.clear(itemPart && itemPart.endNode);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
clear(startNode = this.startNode) {
|
|
895
|
-
removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Implements a boolean attribute, roughly as defined in the HTML
|
|
900
|
-
* specification.
|
|
901
|
-
*
|
|
902
|
-
* If the value is truthy, then the attribute is present with a value of
|
|
903
|
-
* ''. If the value is falsey, the attribute is removed.
|
|
904
|
-
*/
|
|
905
|
-
class BooleanAttributePart {
|
|
906
|
-
constructor(element, name, strings) {
|
|
907
|
-
this.value = undefined;
|
|
908
|
-
this.__pendingValue = undefined;
|
|
909
|
-
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
|
|
910
|
-
throw new Error('Boolean attributes can only contain a single expression');
|
|
911
|
-
}
|
|
912
|
-
this.element = element;
|
|
913
|
-
this.name = name;
|
|
914
|
-
this.strings = strings;
|
|
915
|
-
}
|
|
916
|
-
setValue(value) {
|
|
917
|
-
this.__pendingValue = value;
|
|
918
|
-
}
|
|
919
|
-
commit() {
|
|
920
|
-
while (isDirective(this.__pendingValue)) {
|
|
921
|
-
const directive = this.__pendingValue;
|
|
922
|
-
this.__pendingValue = noChange;
|
|
923
|
-
directive(this);
|
|
924
|
-
}
|
|
925
|
-
if (this.__pendingValue === noChange) {
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
const value = !!this.__pendingValue;
|
|
929
|
-
if (this.value !== value) {
|
|
930
|
-
if (value) {
|
|
931
|
-
this.element.setAttribute(this.name, '');
|
|
932
|
-
} else {
|
|
933
|
-
this.element.removeAttribute(this.name);
|
|
934
|
-
}
|
|
935
|
-
this.value = value;
|
|
936
|
-
}
|
|
937
|
-
this.__pendingValue = noChange;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Sets attribute values for PropertyParts, so that the value is only set once
|
|
942
|
-
* even if there are multiple parts for a property.
|
|
943
|
-
*
|
|
944
|
-
* If an expression controls the whole property value, then the value is simply
|
|
945
|
-
* assigned to the property under control. If there are string literals or
|
|
946
|
-
* multiple expressions, then the strings are expressions are interpolated into
|
|
947
|
-
* a string first.
|
|
948
|
-
*/
|
|
949
|
-
class PropertyCommitter extends AttributeCommitter {
|
|
950
|
-
constructor(element, name, strings) {
|
|
951
|
-
super(element, name, strings);
|
|
952
|
-
this.single = strings.length === 2 && strings[0] === '' && strings[1] === '';
|
|
953
|
-
}
|
|
954
|
-
_createPart() {
|
|
955
|
-
return new PropertyPart(this);
|
|
956
|
-
}
|
|
957
|
-
_getValue() {
|
|
958
|
-
if (this.single) {
|
|
959
|
-
return this.parts[0].value;
|
|
960
|
-
}
|
|
961
|
-
return super._getValue();
|
|
962
|
-
}
|
|
963
|
-
commit() {
|
|
964
|
-
if (this.dirty) {
|
|
965
|
-
this.dirty = false;
|
|
966
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
967
|
-
this.element[this.name] = this._getValue();
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
class PropertyPart extends AttributePart {}
|
|
972
|
-
// Detect event listener options support. If the `capture` property is read
|
|
973
|
-
// from the options object, then options are supported. If not, then the third
|
|
974
|
-
// argument to add/removeEventListener is interpreted as the boolean capture
|
|
975
|
-
// value so we should only pass the `capture` property.
|
|
976
|
-
let eventOptionsSupported = false;
|
|
977
|
-
// Wrap into an IIFE because MS Edge <= v41 does not support having try/catch
|
|
978
|
-
// blocks right into the body of a module
|
|
979
|
-
(() => {
|
|
980
|
-
try {
|
|
981
|
-
const options = {
|
|
982
|
-
get capture() {
|
|
983
|
-
eventOptionsSupported = true;
|
|
984
|
-
return false;
|
|
985
|
-
},
|
|
986
|
-
};
|
|
987
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
988
|
-
window.addEventListener('test', options, options);
|
|
989
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
990
|
-
window.removeEventListener('test', options, options);
|
|
991
|
-
} catch (_e) {
|
|
992
|
-
// event options not supported
|
|
993
|
-
}
|
|
994
|
-
})();
|
|
995
|
-
class EventPart {
|
|
996
|
-
constructor(element, eventName, eventContext) {
|
|
997
|
-
this.value = undefined;
|
|
998
|
-
this.__pendingValue = undefined;
|
|
999
|
-
this.element = element;
|
|
1000
|
-
this.eventName = eventName;
|
|
1001
|
-
this.eventContext = eventContext;
|
|
1002
|
-
this.__boundHandleEvent = (e) => this.handleEvent(e);
|
|
1003
|
-
}
|
|
1004
|
-
setValue(value) {
|
|
1005
|
-
this.__pendingValue = value;
|
|
1006
|
-
}
|
|
1007
|
-
commit() {
|
|
1008
|
-
while (isDirective(this.__pendingValue)) {
|
|
1009
|
-
const directive = this.__pendingValue;
|
|
1010
|
-
this.__pendingValue = noChange;
|
|
1011
|
-
directive(this);
|
|
1012
|
-
}
|
|
1013
|
-
if (this.__pendingValue === noChange) {
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
const newListener = this.__pendingValue;
|
|
1017
|
-
const oldListener = this.value;
|
|
1018
|
-
const shouldRemoveListener =
|
|
1019
|
-
newListener == null ||
|
|
1020
|
-
(oldListener != null &&
|
|
1021
|
-
(newListener.capture !== oldListener.capture || newListener.once !== oldListener.once || newListener.passive !== oldListener.passive));
|
|
1022
|
-
const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener);
|
|
1023
|
-
if (shouldRemoveListener) {
|
|
1024
|
-
this.element.removeEventListener(this.eventName, this.__boundHandleEvent, this.__options);
|
|
1025
|
-
}
|
|
1026
|
-
if (shouldAddListener) {
|
|
1027
|
-
this.__options = getOptions(newListener);
|
|
1028
|
-
this.element.addEventListener(this.eventName, this.__boundHandleEvent, this.__options);
|
|
1029
|
-
}
|
|
1030
|
-
this.value = newListener;
|
|
1031
|
-
this.__pendingValue = noChange;
|
|
1032
|
-
}
|
|
1033
|
-
handleEvent(event) {
|
|
1034
|
-
if (typeof this.value === 'function') {
|
|
1035
|
-
this.value.call(this.eventContext || this.element, event);
|
|
1036
|
-
} else {
|
|
1037
|
-
this.value.handleEvent(event);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
// We copy options because of the inconsistent behavior of browsers when reading
|
|
1042
|
-
// the third argument of add/removeEventListener. IE11 doesn't support options
|
|
1043
|
-
// at all. Chrome 41 only reads `capture` if the argument is an object.
|
|
1044
|
-
const getOptions = (o) => o && (eventOptionsSupported ? { capture: o.capture, passive: o.passive, once: o.once } : o.capture);
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* @license
|
|
1048
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
1049
|
-
* This code may only be used under the BSD style license found at
|
|
1050
|
-
* http://polymer.github.io/LICENSE.txt
|
|
1051
|
-
* The complete set of authors may be found at
|
|
1052
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
1053
|
-
* The complete set of contributors may be found at
|
|
1054
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
1055
|
-
* Code distributed by Google as part of the polymer project is also
|
|
1056
|
-
* subject to an additional IP rights grant found at
|
|
1057
|
-
* http://polymer.github.io/PATENTS.txt
|
|
1058
|
-
*/
|
|
1059
|
-
/**
|
|
1060
|
-
* Creates Parts when a template is instantiated.
|
|
1061
|
-
*/
|
|
1062
|
-
class DefaultTemplateProcessor {
|
|
1063
|
-
/**
|
|
1064
|
-
* Create parts for an attribute-position binding, given the event, attribute
|
|
1065
|
-
* name, and string literals.
|
|
1066
|
-
*
|
|
1067
|
-
* @param element The element containing the binding
|
|
1068
|
-
* @param name The attribute name
|
|
1069
|
-
* @param strings The string literals. There are always at least two strings,
|
|
1070
|
-
* event for fully-controlled bindings with a single expression.
|
|
1071
|
-
*/
|
|
1072
|
-
handleAttributeExpressions(element, name, strings, options) {
|
|
1073
|
-
const prefix = name[0];
|
|
1074
|
-
if (prefix === '.') {
|
|
1075
|
-
const committer = new PropertyCommitter(element, name.slice(1), strings);
|
|
1076
|
-
return committer.parts;
|
|
1077
|
-
}
|
|
1078
|
-
if (prefix === '@') {
|
|
1079
|
-
return [new EventPart(element, name.slice(1), options.eventContext)];
|
|
1080
|
-
}
|
|
1081
|
-
if (prefix === '?') {
|
|
1082
|
-
return [new BooleanAttributePart(element, name.slice(1), strings)];
|
|
1083
|
-
}
|
|
1084
|
-
const committer = new AttributeCommitter(element, name, strings);
|
|
1085
|
-
return committer.parts;
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Create parts for a text-position binding.
|
|
1089
|
-
* @param templateFactory
|
|
1090
|
-
*/
|
|
1091
|
-
handleTextExpression(options) {
|
|
1092
|
-
return new NodePart(options);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
const defaultTemplateProcessor = new DefaultTemplateProcessor();
|
|
1096
|
-
|
|
1097
|
-
/**
|
|
1098
|
-
* @license
|
|
1099
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
1100
|
-
* This code may only be used under the BSD style license found at
|
|
1101
|
-
* http://polymer.github.io/LICENSE.txt
|
|
1102
|
-
* The complete set of authors may be found at
|
|
1103
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
1104
|
-
* The complete set of contributors may be found at
|
|
1105
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
1106
|
-
* Code distributed by Google as part of the polymer project is also
|
|
1107
|
-
* subject to an additional IP rights grant found at
|
|
1108
|
-
* http://polymer.github.io/PATENTS.txt
|
|
1109
|
-
*/
|
|
1110
|
-
/**
|
|
1111
|
-
* The default TemplateFactory which caches Templates keyed on
|
|
1112
|
-
* result.type and result.strings.
|
|
1113
|
-
*/
|
|
1114
|
-
function templateFactory(result) {
|
|
1115
|
-
let templateCache = templateCaches.get(result.type);
|
|
1116
|
-
if (templateCache === undefined) {
|
|
1117
|
-
templateCache = {
|
|
1118
|
-
stringsArray: new WeakMap(),
|
|
1119
|
-
keyString: new Map(),
|
|
1120
|
-
};
|
|
1121
|
-
templateCaches.set(result.type, templateCache);
|
|
1122
|
-
}
|
|
1123
|
-
let template = templateCache.stringsArray.get(result.strings);
|
|
1124
|
-
if (template !== undefined) {
|
|
1125
|
-
return template;
|
|
1126
|
-
}
|
|
1127
|
-
// If the TemplateStringsArray is new, generate a key from the strings
|
|
1128
|
-
// This key is shared between all templates with identical content
|
|
1129
|
-
const key = result.strings.join(marker);
|
|
1130
|
-
// Check if we already have a Template for this key
|
|
1131
|
-
template = templateCache.keyString.get(key);
|
|
1132
|
-
if (template === undefined) {
|
|
1133
|
-
// If we have not seen this key before, create a new Template
|
|
1134
|
-
template = new Template(result, result.getTemplateElement());
|
|
1135
|
-
// Cache the Template for this key
|
|
1136
|
-
templateCache.keyString.set(key, template);
|
|
1137
|
-
}
|
|
1138
|
-
// Cache all future queries for this TemplateStringsArray
|
|
1139
|
-
templateCache.stringsArray.set(result.strings, template);
|
|
1140
|
-
return template;
|
|
1141
|
-
}
|
|
1142
|
-
const templateCaches = new Map();
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* @license
|
|
1146
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
1147
|
-
* This code may only be used under the BSD style license found at
|
|
1148
|
-
* http://polymer.github.io/LICENSE.txt
|
|
1149
|
-
* The complete set of authors may be found at
|
|
1150
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
1151
|
-
* The complete set of contributors may be found at
|
|
1152
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
1153
|
-
* Code distributed by Google as part of the polymer project is also
|
|
1154
|
-
* subject to an additional IP rights grant found at
|
|
1155
|
-
* http://polymer.github.io/PATENTS.txt
|
|
1156
|
-
*/
|
|
1157
|
-
const parts = new WeakMap();
|
|
1158
|
-
/**
|
|
1159
|
-
* Renders a template result or other value to a container.
|
|
1160
|
-
*
|
|
1161
|
-
* To update a container with new values, reevaluate the template literal and
|
|
1162
|
-
* call `render` with the new result.
|
|
1163
|
-
*
|
|
1164
|
-
* @param result Any value renderable by NodePart - typically a TemplateResult
|
|
1165
|
-
* created by evaluating a template tag like `html` or `svg`.
|
|
1166
|
-
* @param container A DOM parent to render to. The entire contents are either
|
|
1167
|
-
* replaced, or efficiently updated if the same result type was previous
|
|
1168
|
-
* rendered there.
|
|
1169
|
-
* @param options RenderOptions for the entire render tree rendered to this
|
|
1170
|
-
* container. Render options must *not* change between renders to the same
|
|
1171
|
-
* container, as those changes will not effect previously rendered DOM.
|
|
1172
|
-
*/
|
|
1173
|
-
const render = (result, container, options) => {
|
|
1174
|
-
let part = parts.get(container);
|
|
1175
|
-
if (part === undefined) {
|
|
1176
|
-
removeNodes(container, container.firstChild);
|
|
1177
|
-
parts.set(container, (part = new NodePart(Object.assign({ templateFactory }, options))));
|
|
1178
|
-
part.appendInto(container);
|
|
1179
|
-
}
|
|
1180
|
-
part.setValue(result);
|
|
1181
|
-
part.commit();
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
/**
|
|
1185
|
-
* @license
|
|
1186
|
-
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
1187
|
-
* This code may only be used under the BSD style license found at
|
|
1188
|
-
* http://polymer.github.io/LICENSE.txt
|
|
1189
|
-
* The complete set of authors may be found at
|
|
1190
|
-
* http://polymer.github.io/AUTHORS.txt
|
|
1191
|
-
* The complete set of contributors may be found at
|
|
1192
|
-
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
1193
|
-
* Code distributed by Google as part of the polymer project is also
|
|
1194
|
-
* subject to an additional IP rights grant found at
|
|
1195
|
-
* http://polymer.github.io/PATENTS.txt
|
|
1196
|
-
*/
|
|
1197
|
-
// IMPORTANT: do not change the property name or the assignment expression.
|
|
1198
|
-
// This line will be used in regexes to search for lit-html usage.
|
|
1199
|
-
// TODO(justinfagnani): inject version number at build time
|
|
1200
|
-
if (typeof window !== 'undefined') {
|
|
1201
|
-
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.4.1');
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Interprets a template literal as an HTML template that can efficiently
|
|
1205
|
-
* render to and update a container.
|
|
1206
|
-
*/
|
|
1207
|
-
const html = (strings, ...values) => new TemplateResult(strings, values, 'html', defaultTemplateProcessor);
|
|
1208
|
-
/**
|
|
1209
|
-
* Interprets a template literal as an SVG template that can efficiently
|
|
1210
|
-
* render to and update a container.
|
|
1211
|
-
*/
|
|
1212
|
-
const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', defaultTemplateProcessor);
|
|
1213
|
-
|
|
1214
|
-
export {
|
|
1215
|
-
AttributeCommitter,
|
|
1216
|
-
AttributePart,
|
|
1217
|
-
BooleanAttributePart,
|
|
1218
|
-
DefaultTemplateProcessor,
|
|
1219
|
-
EventPart,
|
|
1220
|
-
NodePart,
|
|
1221
|
-
PropertyCommitter,
|
|
1222
|
-
PropertyPart,
|
|
1223
|
-
SVGTemplateResult,
|
|
1224
|
-
Template,
|
|
1225
|
-
TemplateInstance,
|
|
1226
|
-
TemplateResult,
|
|
1227
|
-
createMarker,
|
|
1228
|
-
defaultTemplateProcessor,
|
|
1229
|
-
directive,
|
|
1230
|
-
html,
|
|
1231
|
-
isDirective,
|
|
1232
|
-
isIterable,
|
|
1233
|
-
isPrimitive,
|
|
1234
|
-
isTemplatePartActive,
|
|
1235
|
-
noChange,
|
|
1236
|
-
nothing,
|
|
1237
|
-
parts,
|
|
1238
|
-
removeNodes,
|
|
1239
|
-
render,
|
|
1240
|
-
reparentNodes,
|
|
1241
|
-
svg,
|
|
1242
|
-
templateCaches,
|
|
1243
|
-
templateFactory,
|
|
1244
|
-
};
|
package.json
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"lit-html"
|
|
15
15
|
],
|
|
16
16
|
"files": [
|
|
17
|
-
"lit-html.js",
|
|
18
17
|
"index.js",
|
|
19
18
|
"index.d.ts"
|
|
20
19
|
],
|
|
@@ -22,17 +21,16 @@
|
|
|
22
21
|
"author": "pyrossh",
|
|
23
22
|
"type": "module",
|
|
24
23
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
24
|
+
"node": ">=22.3.0"
|
|
26
25
|
},
|
|
27
26
|
"scripts": {
|
|
28
|
-
"example": "node
|
|
27
|
+
"example": "node examples/server.js",
|
|
28
|
+
"test": "node --test index.test.js",
|
|
29
|
+
"test:update-snapshots": "node --test --test-update-snapshots index.test.js",
|
|
29
|
-
"test": "
|
|
30
|
+
"test:e2e": "node --test examples/e2e.spec.js"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"
|
|
33
|
+
"playwright": "^1.62.1"
|
|
33
|
-
},
|
|
34
|
-
"jest": {
|
|
35
|
-
"transform": {}
|
|
36
34
|
},
|
|
37
35
|
"prettier": {
|
|
38
36
|
"printWidth": 160,
|
|
@@ -41,6 +39,7 @@
|
|
|
41
39
|
"trailingComma": "all"
|
|
42
40
|
},
|
|
43
41
|
"dependencies": {
|
|
42
|
+
"lit-html": "^3.3.3",
|
|
44
43
|
"mutative": "^1.3.0"
|
|
45
44
|
}
|
|
46
45
|
}
|