@webtides/element-js 0.7.3 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ import { Part } from './Part.js';
2
+
3
+ /**
4
+ * @param {Element} node
5
+ * @param {String} name
6
+ * @param {Boolean} oldValue
7
+ * @return {(function(*): void)|*}
8
+ */
9
+ const processBooleanAttribute = (node, name, oldValue) => {
10
+ // TODO: It would be better if the ?boolean= attribute would not be there in the first place...
11
+ node.removeAttribute(`?${name}`);
12
+ return (newValue) => {
13
+ const value = !!newValue?.valueOf() && newValue !== 'false';
14
+ if (oldValue !== value) {
15
+ node.toggleAttribute(name, (oldValue = !!value));
16
+ }
17
+ };
18
+ };
19
+
20
+ /**
21
+ * @param {Element} node
22
+ * @param {String} name
23
+ * @return {(function(*): void)|*}
24
+ */
25
+ const processPropertyAttribute = (node, name) => {
26
+ // TODO: It would be better if the .property= attribute would not be there in the first place...
27
+ node.removeAttribute(`.${name}`);
28
+ return (value) => {
29
+ node[name] = value;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * @param {Element} node
35
+ * @param {String} name
36
+ * @return {(function(*): void)|*}
37
+ */
38
+ const processEventAttribute = (node, name) => {
39
+ // TODO: It would be better if the event attribute would not be there in the first place...
40
+ node.removeAttribute(name);
41
+
42
+ let oldValue = undefined;
43
+ let type = name.startsWith('@') ? name.slice(1) : name.toLowerCase().slice(2);
44
+
45
+ return (newValue) => {
46
+ if (oldValue !== newValue) {
47
+ if (oldValue) node.removeEventListener(type, oldValue);
48
+ if ((oldValue = newValue)) node.addEventListener(type, oldValue);
49
+ }
50
+ };
51
+ };
52
+
53
+ /**
54
+ * @param {Element} node
55
+ * @param {String} name
56
+ * @return {(function(*): void)|*}
57
+ */
58
+ const processAttribute = (node, name) => {
59
+ let oldValue,
60
+ orphan = true;
61
+ const attributeNode = globalThis.document?.createAttributeNS(null, name);
62
+ return (newValue) => {
63
+ const value = newValue?.valueOf();
64
+ if (oldValue !== value) {
65
+ if ((oldValue = value) == null) {
66
+ if (!orphan) {
67
+ node.removeAttributeNode(attributeNode);
68
+ orphan = true;
69
+ }
70
+ } else {
71
+ attributeNode.value = value;
72
+ if (orphan) {
73
+ node.setAttributeNodeNS(attributeNode);
74
+ orphan = false;
75
+ }
76
+ }
77
+ }
78
+ };
79
+ };
80
+
81
+ /**
82
+ * @param {Element} node
83
+ * @param {String} name
84
+ * @return {(function(*): void)|*}
85
+ */
86
+ export const processAttributePart = (node, name) => {
87
+ // boolean attribute: ?boolean=${...}
88
+ if (name.startsWith('?')) {
89
+ return processBooleanAttribute(node, name.slice(1), false);
90
+ }
91
+
92
+ // property attribute: .property=${...}
93
+ if (name.startsWith('.')) {
94
+ return processPropertyAttribute(node, name.slice(1));
95
+ }
96
+
97
+ // event attribute: @event=${...} || "old school" event attribute: onevent=${...}
98
+ if (name.startsWith('@') || name.startsWith('on')) {
99
+ return processEventAttribute(node, name);
100
+ }
101
+
102
+ // normal "string" attribute: attribute=${...}
103
+ return processAttribute(node, name);
104
+ };
105
+
106
+ /**
107
+ * For updating a single attribute
108
+ */
109
+ export class AttributePart extends Part {
110
+ name = undefined;
111
+ interpolations = 1;
112
+ values = [];
113
+ currentValueIndex = 0;
114
+ initialValue = undefined;
115
+
116
+ /**
117
+ * @param {Node} node
118
+ * @param {String} name
119
+ * @param {String} initialValue
120
+ */
121
+ constructor(node, name, initialValue) {
122
+ super();
123
+ this.name = name;
124
+ this.initialValue = initialValue;
125
+ this.processor = processAttributePart(node.nextElementSibling, this.name);
126
+ }
127
+
128
+ /**
129
+ * @param {Node} node
130
+ */
131
+ addNode(node) {
132
+ this.interpolations++;
133
+ // TODO: what about the comment node? It will always be the first for now...
134
+ }
135
+
136
+ /**
137
+ * @param {string | number | bigint | boolean | undefined | symbol | null} value
138
+ */
139
+ update(value) {
140
+ // If we only have one sole interpolation, we can just apply the update
141
+ if (this.initialValue === '\x03') {
142
+ this.processor(value);
143
+ return;
144
+ }
145
+
146
+ // Instead of applying the update immediately, we check if the part has multiple interpolations
147
+ // and store the values for each interpolation in a list
148
+ this.values[this.currentValueIndex++] = value;
149
+
150
+ // Only the last call to update (for the last interpolation) will actually trigger the update
151
+ // on the DOM processor. Here we can reset everything before the next round of updates
152
+ if (this.interpolations === this.currentValueIndex) {
153
+ this.currentValueIndex = 0;
154
+ let replaceIndex = 0;
155
+ // Note: this will coarse the values into strings, but it's probably ok since there can only be multiple values in string attributes?!
156
+ const parsedValue = this.initialValue.replace(/\x03/g, () => this.values[replaceIndex++]);
157
+ this.processor(parsedValue);
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,343 @@
1
+ import { COMMENT_NODE, ELEMENT_NODE } from '../util/DOMHelper.js';
2
+ import { Part } from './Part.js';
3
+ import { TemplateResult } from './TemplateResult.js';
4
+ import { TemplatePart } from './TemplatePart.js';
5
+
6
+ /**
7
+ * @param {Node | TemplatePart | any[]} node
8
+ * @return {ChildNode}
9
+ */
10
+ const removeChildNodes = (node) => {
11
+ const range = globalThis.document?.createRange();
12
+ const firstChild = Array.isArray(node)
13
+ ? node[0]
14
+ : node instanceof TemplatePart
15
+ ? node.childNodes[0]
16
+ : node.firstChild;
17
+ const lastChild = Array.isArray(node)
18
+ ? node[node.length - 1]
19
+ : node instanceof TemplatePart
20
+ ? node.childNodes[node.childNodes.length - 1]
21
+ : node.lastChild;
22
+ range.setStartAfter(firstChild);
23
+ range.setEndAfter(lastChild);
24
+ range.deleteContents();
25
+ return firstChild;
26
+ };
27
+
28
+ /**
29
+ * @param {Element} commentNode
30
+ */
31
+ const removeNodesBetweenComments = (commentNode) => {
32
+ while (commentNode.previousSibling) {
33
+ if (
34
+ commentNode.previousSibling.nodeType === COMMENT_NODE &&
35
+ commentNode.previousSibling.data === commentNode.data.replace('/', '')
36
+ ) {
37
+ break;
38
+ }
39
+ commentNode.parentNode.removeChild(commentNode.previousSibling);
40
+ }
41
+ };
42
+
43
+ /**
44
+ * @param {Node} parentNode
45
+ * @param {Node[]} domChildNodes
46
+ * @param {Node[]} templateChildNodes
47
+ * @param {Node} anchorNode
48
+ * @return {Node[]}
49
+ */
50
+ const diffChildNodes = function (parentNode, domChildNodes, templateChildNodes, anchorNode) {
51
+ // Diff each node in the child node lists
52
+ let length = Math.max(templateChildNodes.length, domChildNodes.length);
53
+ for (let index = 0; index < length; index++) {
54
+ let domChildNode = domChildNodes[index];
55
+ let templateChildNode = templateChildNodes[index];
56
+
57
+ // If the DOM node doesn't exist, append/copy the template node
58
+ if (!domChildNode) {
59
+ if (templateChildNode instanceof TemplatePart) {
60
+ anchorNode.before(...templateChildNode.childNodes);
61
+ } else if (typeof templateChildNode === 'object' && 'ELEMENT_NODE' in templateChildNode) {
62
+ parentNode.insertBefore(templateChildNode, anchorNode);
63
+ } else {
64
+ // TODO: this might not be performant?! Can we maybe handle this in processDomNode?!
65
+ // TODO: I think that we need to make ChildNodeParts from all primitive values as well
66
+ parentNode.insertBefore(document.createTextNode(templateChildNode), anchorNode);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ // If the template node doesn't exist, remove the node in the DOM
72
+ if (!templateChildNode) {
73
+ parentNode.removeChild(removeChildNodes(domChildNode));
74
+ continue;
75
+ }
76
+
77
+ // If DOM node is equal to the template node, don't do anything
78
+ // if (domChildNode.isEqualNode(templateChildNode)) {
79
+ if (domChildNode === templateChildNode) {
80
+ continue;
81
+ }
82
+
83
+ // If node types are not the same, replace the DOM node with the template node
84
+ if (templateChildNode.nodeType !== domChildNode.nodeType) {
85
+ parentNode.replaceChild(templateChildNode, domChildNode);
86
+ continue;
87
+ }
88
+
89
+ // If the element tag names are not the same, replace the DOM node with the template node
90
+ if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName !== domChildNode.tagName) {
91
+ parentNode.replaceChild(templateChildNode, domChildNode);
92
+ continue;
93
+ }
94
+
95
+ // If the node is an SVG element, don't even think about diffing it, just replace it
96
+ if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName === 'SVG') {
97
+ parentNode.replaceChild(templateChildNode, domChildNode);
98
+ continue;
99
+ }
100
+ }
101
+ return templateChildNodes;
102
+ };
103
+
104
+ /**
105
+ * @param {Node} comment
106
+ * @param {TemplatePart | any[] | any} initialValue
107
+ * @return {(function(*): void)|*}
108
+ */
109
+ export const processNodePart = (comment, initialValue) => {
110
+ let nodes = [];
111
+ let oldValue = initialValue instanceof TemplatePart ? [...initialValue.childNodes] : initialValue;
112
+ // this is for string values to be inserted into the DOM. A cached TextNode will be used so that we don't have to constantly create new DOM nodes.
113
+ let cachedTextNode = undefined;
114
+
115
+ const processNodeValue = (newValue) => {
116
+ switch (typeof newValue) {
117
+ // primitives are handled as text content
118
+ case 'string':
119
+ case 'number':
120
+ case 'boolean':
121
+ if (oldValue !== newValue) {
122
+ oldValue = newValue;
123
+ if (!cachedTextNode) cachedTextNode = globalThis.document?.createTextNode('');
124
+ cachedTextNode.data = newValue;
125
+
126
+ if (comment.previousSibling?.data === comment.data.replace('/', '')) {
127
+ // the part is empty - we haven't rendered it yet
128
+ comment.parentNode.insertBefore(cachedTextNode, comment);
129
+ } else {
130
+ // the part was already rendered (either server side or on the client)
131
+ comment.previousSibling.data = newValue;
132
+ }
133
+
134
+ nodes = [cachedTextNode];
135
+ }
136
+ break;
137
+ // null (= typeof "object") and undefined are used to clean up previous content
138
+ case 'object':
139
+ case 'undefined':
140
+ if (newValue === null || newValue === undefined) {
141
+ if (oldValue !== newValue) {
142
+ oldValue = newValue;
143
+ removeNodesBetweenComments(comment);
144
+ nodes = [];
145
+ }
146
+ break;
147
+ }
148
+ if (Array.isArray(newValue)) {
149
+ if (newValue.length === 0) {
150
+ removeNodesBetweenComments(comment);
151
+ nodes = [];
152
+ } else {
153
+ nodes = diffChildNodes(comment.parentNode, oldValue || [], newValue, comment);
154
+ }
155
+ oldValue = newValue;
156
+ break;
157
+ }
158
+ // if the new value is a node or a fragment, and it's different from the live node, then it's diffed.
159
+ if (oldValue !== newValue) {
160
+ if ('ELEMENT_NODE' in newValue) {
161
+ oldValue = newValue;
162
+ nodes = diffChildNodes(comment.parentNode, nodes, [newValue], comment);
163
+ } else if (newValue instanceof TemplatePart) {
164
+ oldValue = newValue;
165
+ nodes = diffChildNodes(comment.parentNode, nodes, [...newValue.childNodes], comment);
166
+ } else {
167
+ console.warn('Could not process value', newValue);
168
+ }
169
+ }
170
+ break;
171
+ case 'function':
172
+ processNodeValue(newValue(comment));
173
+ break;
174
+ }
175
+ };
176
+
177
+ return processNodeValue;
178
+ };
179
+
180
+ /**
181
+ * For updating a text node, a sequence of child nodes or an array of `any` values
182
+ */
183
+ export class ChildNodePart extends Part {
184
+ /** @type {Node | undefined} */
185
+ startNode = undefined;
186
+
187
+ /** @type {Node | undefined} */
188
+ endNode = undefined;
189
+
190
+ // TODO: this is only for array values
191
+ /** @type {Part[]} */
192
+ parts = [];
193
+
194
+ // TODO: this is only for TemplatePart values
195
+ templatePart = undefined;
196
+
197
+ // TODO: maybe we can store a generic vale|parsedValue that can be either parts[] | templatePart | any
198
+
199
+ /** @type {Node[]} */
200
+ childNodes = [];
201
+
202
+ /**
203
+ * @param {Node} startNode - the start comment marker node
204
+ * @param {any | any[]} value
205
+ */
206
+ constructor(startNode, value) {
207
+ if (startNode && startNode?.nodeType !== COMMENT_NODE) {
208
+ throw new Error('ChildNodePart: startNode is not a comment node');
209
+ }
210
+
211
+ super();
212
+
213
+ let serverSideRendered = false;
214
+ if (startNode) {
215
+ const placeholder = startNode.data;
216
+ const childNodes = [startNode];
217
+ let childNode = startNode.nextSibling;
218
+ while (childNode && childNode.data !== `/${placeholder}`) {
219
+ childNodes.push(childNode);
220
+ childNode = childNode.nextSibling;
221
+ }
222
+
223
+ const endNode = childNode;
224
+ childNodes.push(endNode);
225
+
226
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
227
+ if (childNodes.length > 2) {
228
+ serverSideRendered = true;
229
+ }
230
+
231
+ this.childNodes = childNodes;
232
+ this.startNode = startNode;
233
+ this.endNode = endNode;
234
+ }
235
+
236
+ // value can become array | TemplatePart | any
237
+ const initialValue = this.parseValue(value);
238
+
239
+ if (this.endNode) {
240
+ this.processor = processNodePart(this.endNode, serverSideRendered ? initialValue : undefined);
241
+ }
242
+
243
+ if (!serverSideRendered) {
244
+ if (value instanceof TemplateResult || Array.isArray(value)) {
245
+ this.updateParts(value);
246
+ }
247
+
248
+ // We need a childNodes list that is NOT live so that we don't lose elements when they get removed from the dom and we can (re)add them back in later.
249
+ this.childNodes = [...this.childNodes];
250
+ this.processor?.(initialValue);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * @param {any} value
256
+ * @return {*}
257
+ */
258
+ update(value) {
259
+ const parsedValue = this.parseValue(value);
260
+
261
+ if (value instanceof TemplateResult || Array.isArray(value)) {
262
+ this.updateParts(value);
263
+ }
264
+
265
+ this.processor?.(parsedValue);
266
+ }
267
+
268
+ /**
269
+ * @param {TemplateResult | any[]} value
270
+ */
271
+ updateParts(value) {
272
+ if (value instanceof TemplateResult) {
273
+ this.templatePart.update(value);
274
+ }
275
+ if (Array.isArray(value)) {
276
+ for (let index = 0; index < value.length; index++) {
277
+ this.parts[index]?.update(value[index]);
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @param {TemplateResult | any[] | any} value
284
+ * @return {TemplatePart | any[] | any}
285
+ */
286
+ parseValue(value) {
287
+ if (value instanceof TemplateResult) {
288
+ if (!this.templatePart) {
289
+ const templatePartCommentNodes = this.childNodes?.filter(
290
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
291
+ );
292
+ const startNode = templatePartCommentNodes[0];
293
+ this.templatePart = new TemplatePart(startNode, value);
294
+ }
295
+ return this.templatePart;
296
+ }
297
+ if (Array.isArray(value)) {
298
+ return this.parseArray(value);
299
+ }
300
+ return value;
301
+ }
302
+
303
+ /**
304
+ * Nested TemplateResults values need to be unrolled in order for update functions to be able to process them
305
+ * @param {any[]} values
306
+ * @return {any[]}
307
+ */
308
+ parseArray(values) {
309
+ const parsedValues = [];
310
+ for (let index = 0; index < values.length; index++) {
311
+ let value = values[index];
312
+
313
+ if (value instanceof TemplateResult) {
314
+ let templatePart = this.parts[index];
315
+ if (!templatePart) {
316
+ const templatePartCommentNodes = this.childNodes?.filter(
317
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
318
+ );
319
+ const startNode = templatePartCommentNodes[index];
320
+ templatePart = new TemplatePart(startNode, value);
321
+ this.parts[index] = templatePart;
322
+ }
323
+ parsedValues[index] = templatePart;
324
+ } else if (Array.isArray(value)) {
325
+ let childNodePart = this.parts[index];
326
+ if (!childNodePart) {
327
+ // TODO: this seems not correct :(
328
+ const templatePartCommentNodes = this.childNodes?.filter(
329
+ (node) => node && node.nodeType === COMMENT_NODE && node.data === 'template-part',
330
+ );
331
+ const startNode = templatePartCommentNodes[index];
332
+ childNodePart = new ChildNodePart(startNode, value);
333
+ this.parts[index] = childNodePart;
334
+ }
335
+ parsedValues[index] = childNodePart;
336
+ } else {
337
+ parsedValues[index] = typeof value === 'function' ? value() : value;
338
+ }
339
+ }
340
+
341
+ return parsedValues;
342
+ }
343
+ }
@@ -0,0 +1,37 @@
1
+ import { Part } from './Part.js';
2
+ import { isObjectLike } from '../util/AttributeParser.js';
3
+
4
+ /**
5
+ * For updating a directive on a node
6
+ */
7
+ export class NodePart extends Part {
8
+ /** @type {Node} */
9
+ node = undefined;
10
+
11
+ /** @type {Directive} */
12
+ directive = undefined;
13
+
14
+ /**
15
+ * @param {Node} node
16
+ * @param {() => {}} initialValue
17
+ */
18
+ constructor(node, initialValue) {
19
+ if (!isObjectLike(initialValue) && !initialValue.directiveClass) {
20
+ throw new Error(
21
+ 'NodePart: value is not a wrapped directive function. You must wrap you custom directive class with the defineDirective function.',
22
+ );
23
+ }
24
+ super();
25
+ this.node = node.nextElementSibling;
26
+ const { directiveClass, values } = initialValue;
27
+ this.directive = new directiveClass(node.nextElementSibling);
28
+ }
29
+
30
+ /**
31
+ * @param {() => {}} value
32
+ */
33
+ update(value) {
34
+ const { directiveClass, values } = value;
35
+ this.directive.update(...values);
36
+ }
37
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @abstract
3
+ */
4
+ export class Part {
5
+ processor = undefined;
6
+
7
+ /**
8
+ * @abstract
9
+ * @param {TemplateResult | any[] | any} value
10
+ */
11
+ update(value) {}
12
+ }
@@ -0,0 +1,140 @@
1
+ import { COMMENT_NODE, convertStringToTemplate } from '../util/DOMHelper.js';
2
+ import { Part } from './Part.js';
3
+ import { TemplateResult } from './TemplateResult.js';
4
+ import { AttributePart } from './AttributePart.js';
5
+ import { ChildNodePart } from './ChildNodePart.js';
6
+ import { NodePart } from './NodePart.js';
7
+
8
+ /** @type {Map<TemplateStringsArray, Part[]>} */
9
+ const partsCache = new WeakMap();
10
+
11
+ /** @type {Map<TemplateStringsArray, DocumentFragment>} */
12
+ const fragmentsCache = new WeakMap();
13
+
14
+ export class TemplatePart extends Part {
15
+ /** @type {Part[]} */
16
+ parts = [];
17
+
18
+ /** @type {TemplateStringsArray} */
19
+ strings = undefined;
20
+
21
+ /** @type {Node[]} */
22
+ childNodes = [];
23
+
24
+ /**
25
+ * @param {Node} startNode - the start comment marker node
26
+ * @param {TemplateResult} value
27
+ */
28
+ constructor(startNode, value) {
29
+ if (startNode && startNode?.nodeType !== COMMENT_NODE) {
30
+ throw new Error('TemplatePart: startNode is not a comment node');
31
+ }
32
+
33
+ super();
34
+
35
+ let serverSideRendered = false;
36
+ if (startNode) {
37
+ const placeholder = startNode.data;
38
+ const childNodes = [startNode];
39
+ let childNode = startNode.nextSibling;
40
+ while (childNode && childNode.data !== `/${placeholder}`) {
41
+ childNodes.push(childNode);
42
+ childNode = childNode.nextSibling;
43
+ }
44
+
45
+ const endNode = childNode;
46
+ childNodes.push(endNode);
47
+
48
+ // if not SSRed, childNodes will only ever have two comment nodes, the start and the end marker
49
+ if (childNodes.length > 2) {
50
+ serverSideRendered = true;
51
+ this.childNodes = childNodes;
52
+ }
53
+ }
54
+
55
+ this.parseValue(value);
56
+
57
+ if (!serverSideRendered) {
58
+ this.updateParts(value.values);
59
+ // We need a childNodes list that is NOT live so that we don't loose elements when they get removed from the dom and we can (re)add them back in later.
60
+ this.childNodes = [...this.childNodes];
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param {TemplateResult} value
66
+ */
67
+ update(value) {
68
+ this.parseValue(value);
69
+ this.updateParts(value.values);
70
+ }
71
+
72
+ /**
73
+ * @param {any[]} values
74
+ */
75
+ updateParts(values) {
76
+ for (let index = 0; index < values.length; index++) {
77
+ this.parts[index]?.update(values[index]);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @param {TemplateResult} templateResult
83
+ */
84
+ parseValue(templateResult) {
85
+ if (this.strings !== templateResult.strings) {
86
+ if (this.childNodes.length === 0) {
87
+ let fragment = fragmentsCache.get(templateResult.strings);
88
+ if (!fragment) {
89
+ fragment = this.parseFragment(templateResult);
90
+ fragmentsCache.set(templateResult.strings, fragment);
91
+ }
92
+ const importedFragment = globalThis.document?.importNode(fragment, true);
93
+ this.childNodes = importedFragment.childNodes;
94
+ }
95
+
96
+ let parts = partsCache.get(templateResult.strings);
97
+ if (!parts) {
98
+ parts = templateResult.parseParts(this.childNodes);
99
+ partsCache.set(templateResult.strings, parts);
100
+ }
101
+
102
+ /** @type AttributePart */
103
+ let previousAttributePart = undefined;
104
+ this.parts = parts.map((part, index) => {
105
+ // We currently need the path because the fragment will be cloned via importNode and therefore the node will be a different one
106
+ const node = part.path.reduceRight(({ childNodes }, i) => childNodes[i], this);
107
+
108
+ if (part.type === 'node') {
109
+ return new ChildNodePart(node, templateResult.values[index]);
110
+ // TODO: the nodes in the parts array also have nested parts information... could we start creating the nested parts here as well?
111
+ }
112
+ if (part.type === 'attribute') {
113
+ // If we have multiple attribute parts with the same name, it means we have multiple
114
+ // interpolations inside that attribute. Instead of creating a new part, we will return the same
115
+ // as before and let it defer the update until the last interpolation gets updated
116
+ if (previousAttributePart && previousAttributePart.name === part.name) {
117
+ previousAttributePart.addNode(node);
118
+ return previousAttributePart;
119
+ }
120
+ return (previousAttributePart = new AttributePart(node, part.name, part.initialValue));
121
+ }
122
+ if (part.type === 'directive') {
123
+ return new NodePart(node, templateResult.values[index]);
124
+ }
125
+
126
+ throw `cannot map part: ${part}`;
127
+ });
128
+ this.strings = templateResult.strings;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @param {TemplateResult} templateResult
134
+ * @return {DocumentFragment}
135
+ */
136
+ parseFragment(templateResult) {
137
+ const templateString = templateResult.templateString;
138
+ return convertStringToTemplate(templateString);
139
+ }
140
+ }