@webtides/element-js 0.3.2-rc.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,18 +1,25 @@
1
1
  {
2
2
  "name": "@webtides/element-js",
3
- "version": "0.3.2-rc.6",
3
+ "version": "0.4.1",
4
4
  "module": "index.js",
5
5
  "main": "dist/index.js",
6
- "exports": {
7
- "browser": "./dist/index.js",
8
- "import": "./index.js",
9
- "default": "./index.js"
10
- },
11
6
  "files": [
12
7
  "/dist",
13
8
  "/src",
14
9
  "index.js"
15
10
  ],
11
+ "exports": {
12
+ ".": {
13
+ "browser": "./dist/index.js",
14
+ "import": "./index.js",
15
+ "default": "./index.js"
16
+ },
17
+ "./src/*": "./src/*.js",
18
+ "./src/util/*": "./src/util/*.js",
19
+ "./src/renderer/vanilla": "./src/renderer/vanilla/index.js",
20
+ "./src/renderer/vanilla/TemplateElement": "./src/renderer/vanilla/TemplateElement.js",
21
+ "./src/renderer/vanilla/util/*": "./src/renderer/vanilla/util/*.js"
22
+ },
16
23
  "repository": "https://github.com/webtides/element-js.git",
17
24
  "author": "@webtides",
18
25
  "license": "MIT",
@@ -31,9 +38,8 @@
31
38
  "lit-html": "^1.2.1"
32
39
  },
33
40
  "devDependencies": {
34
- "@open-wc/semantic-dom-diff": "^0.17.9",
35
- "@open-wc/testing": "^2.5.16",
36
- "@open-wc/testing-karma": "^3.3.19",
41
+ "@open-wc/testing": "^3.0.3",
42
+ "@web/test-runner": "^0.13.26",
37
43
  "babel-eslint": "^10.1.0",
38
44
  "babel-plugin-module-resolver": "^4.0.0",
39
45
  "deepmerge": "^4.2.2",
@@ -41,15 +47,9 @@
41
47
  "eslint": "^7.0.0",
42
48
  "eslint-config-prettier": "^6.11.0",
43
49
  "eslint-plugin-prettier": "^3.1.3",
44
- "husky": "^4.2.5",
45
- "karma": "^5.0.5",
46
- "lint-staged": "^10.2.2",
47
- "prettier": "^2.0.5"
48
- },
49
- "husky": {
50
- "hooks": {
51
- "pre-commit": "lint-staged"
52
- }
50
+ "husky": "^7.0.4",
51
+ "lint-staged": "^12.1.2",
52
+ "prettier": "^2.5.1"
53
53
  },
54
54
  "lint-staged": {
55
55
  "*.{js,json}": [
@@ -60,8 +60,8 @@
60
60
  "scripts": {
61
61
  "lint": "eslint src/**/*.js",
62
62
  "prepare": "esbuild index.js --outfile=dist/index.js --bundle --format=esm --sourcemap --target=chrome79,firefox68,safari12,edge79",
63
- "test": "karma start",
64
- "test:coverage": "karma start --coverage",
65
- "test:watch": "karma start --auto-watch=true --single-run=false"
63
+ "test": "web-test-runner test/**/*.test.js --node-resolve",
64
+ "test:coverage": "web-test-runner test/**/*.test.js --node-resolve --coverage",
65
+ "test:watch": "web-test-runner test/**/*.test.js --node-resolve --watch"
66
66
  }
67
67
  }
@@ -1,5 +1,6 @@
1
1
  import { parseAttribute, isNaN, dashToCamel, camelToDash, isObjectLike } from './util/AttributeParser.js';
2
2
  import { getClosestParentCustomElementNode, isOfSameNodeType } from './util/DOMHelper.js';
3
+
3
4
  export { defineElement } from './util/defineElement';
4
5
  export { toString } from './util/toString';
5
6
 
@@ -369,9 +370,16 @@ class BaseElement extends HTMLElement {
369
370
 
370
371
  eventTargets.forEach((eventTarget) => {
371
372
  Object.keys(events).forEach((eventName) => {
372
- const callback = events[eventName].bind(this);
373
- eventTarget.addEventListener(eventName, callback);
374
- this._registeredEvents.push({ eventTarget, eventName, callback });
373
+ const notation = events[eventName];
374
+ if (typeof notation === 'function') {
375
+ const callback = notation.bind(this);
376
+ eventTarget.addEventListener(eventName, callback);
377
+ this._registeredEvents.push({ eventTarget, eventName, callback });
378
+ } else if (typeof notation === 'object' && typeof notation.listener === 'function') {
379
+ const callback = notation.listener.bind(this);
380
+ eventTarget.addEventListener(eventName, callback, notation.options ?? {});
381
+ this._registeredEvents.push({ eventTarget, eventName, callback });
382
+ }
375
383
  });
376
384
  });
377
385
  });
@@ -400,7 +408,23 @@ class BaseElement extends HTMLElement {
400
408
 
401
409
  refsArray.forEach((refNode) => {
402
410
  const refKey = refNode.getAttribute('ref');
403
- refsMap[refKey] = refNode;
411
+ const isListKey = refKey.endsWith('[]');
412
+ const cleanKey = isListKey ? refKey.slice(0, -2) : refKey;
413
+ const hasListRef = Array.isArray(refsMap[cleanKey]);
414
+
415
+ if (isListKey) {
416
+ if (!refsMap[cleanKey] || !hasListRef) {
417
+ // create or overwrite previously registered single ref
418
+ refsMap[cleanKey] = [];
419
+ }
420
+ refsMap[cleanKey].push(cleanKey);
421
+ } else if (!hasListRef) {
422
+ refsMap[refKey] = refNode;
423
+ } else {
424
+ console.warn(
425
+ `Did not register singular ref ${refKey}, ${refNode} as a ref list is already registered under the same name.`,
426
+ );
427
+ }
404
428
  });
405
429
 
406
430
  this.$refs = refsMap;
@@ -0,0 +1,52 @@
1
+ import { StyledElement } from '../../StyledElement';
2
+ import { html, spreadAttributes, unsafeHTML } from './util/html';
3
+ import { render } from './util/render';
4
+ export { i18n } from '../../util/i18n';
5
+
6
+ class TemplateElement extends StyledElement {
7
+ static _$templateElement$ = true;
8
+
9
+ constructor(options) {
10
+ super({
11
+ deferUpdate: false,
12
+ shadowRender: false,
13
+ styles: [],
14
+ adoptGlobalStyles: true,
15
+ mutationObserverOptions: {
16
+ childList: false,
17
+ },
18
+ ...options,
19
+ });
20
+ this._template = this._options.template;
21
+
22
+ if (this._options.shadowRender && !this.shadowRoot) {
23
+ this.attachShadow({ mode: 'open' });
24
+ }
25
+ }
26
+
27
+ template() {
28
+ return html``;
29
+ }
30
+
31
+ update(options) {
32
+ this.renderTemplate();
33
+ super.update(options);
34
+ }
35
+
36
+ renderTemplate() {
37
+ const template = this._template || this.template();
38
+
39
+ if (typeof template === 'string') {
40
+ // just a plain string literal. no fancy rendering required
41
+ this.getRoot().innerHTML = `${template}`;
42
+ } else {
43
+ render(template, this.getRoot());
44
+ }
45
+ }
46
+
47
+ getRoot() {
48
+ return this.shadowRoot !== null ? this.shadowRoot : this;
49
+ }
50
+ }
51
+
52
+ export { TemplateElement, html, unsafeHTML, render, spreadAttributes };
@@ -0,0 +1,18 @@
1
+ import { BaseElement } from '../../BaseElement';
2
+ import { StyledElement } from '../../StyledElement';
3
+ import { TemplateElement, html, unsafeHTML, spreadAttributes } from './TemplateElement';
4
+ import { toString } from '../../util/toString';
5
+ import { defineElement } from '../../util/defineElement';
6
+ import { i18n } from '../../util/i18n';
7
+
8
+ export {
9
+ BaseElement,
10
+ StyledElement,
11
+ TemplateElement,
12
+ html,
13
+ unsafeHTML,
14
+ spreadAttributes,
15
+ toString,
16
+ defineElement,
17
+ i18n,
18
+ };
@@ -0,0 +1,78 @@
1
+ import { camelToDash, decodeAttribute, encodeAttribute } from '../../../util/AttributeParser';
2
+
3
+ class Part {
4
+ constructor(value) {
5
+ this.value = value;
6
+ }
7
+
8
+ toString() {
9
+ if (typeof this.value !== 'string') {
10
+ return encodeAttribute(JSON.stringify(this.value));
11
+ }
12
+ return encodeAttribute(this.value);
13
+ }
14
+ }
15
+
16
+ class TemplatePart extends Part {
17
+ $$templatePart = true;
18
+
19
+ constructor(value) {
20
+ super(value?.$$templatePart ? value.value : value);
21
+ }
22
+
23
+ toString() {
24
+ if (this.value === null || this.value === undefined) {
25
+ return '';
26
+ } else if (this.value === '') {
27
+ return '';
28
+ } else if (Array.isArray(this.value)) {
29
+ return this.value.map((row) => new TemplatePart(row).toString()).join('');
30
+ } else if (this.value?.$$templateLiteral) {
31
+ return this.value.toString();
32
+ } else if (typeof this.value === 'function') {
33
+ // A directive
34
+ return this.value(this);
35
+ }
36
+ return super.toString();
37
+ }
38
+ }
39
+
40
+ class TemplateLiteral {
41
+ $$templateLiteral = true;
42
+
43
+ constructor(strings, ...values) {
44
+ let result = [];
45
+ for (let i = 0; i < strings.length; i++) {
46
+ result.push(strings[i]);
47
+
48
+ if (i < values.length) {
49
+ result.push(new TemplatePart(values[i]));
50
+ }
51
+ }
52
+ this.result = result;
53
+ }
54
+
55
+ toString() {
56
+ return `${this.result.join('')}`;
57
+ }
58
+ }
59
+
60
+ const html = function (strings, ...values) {
61
+ return new TemplateLiteral(strings, ...values);
62
+ };
63
+
64
+ const unsafeHTML = (string) => {
65
+ return () => `<!--$$plainly-set-inner-html-->${decodeAttribute(string)}`;
66
+ };
67
+
68
+ const spreadAttributes = (attributes) => {
69
+ return () => {
70
+ return Object.keys(attributes)
71
+ .map((key) => {
72
+ return `${camelToDash(key)}='${new Part(attributes[key])}'`;
73
+ })
74
+ .join(' ');
75
+ };
76
+ };
77
+
78
+ export { html, unsafeHTML, spreadAttributes };
@@ -0,0 +1,278 @@
1
+ const NODE_TYPE_COMMENT = 8;
2
+ const NODE_TYPE_TEXT = 3;
3
+
4
+ const _cachedTemplateElements = {};
5
+
6
+ const convertStringToHTML = (templateString) => {
7
+ const parser = new DOMParser();
8
+ const document = parser.parseFromString(`<main>${templateString}</main>`, 'text/html');
9
+
10
+ return document.querySelector('main');
11
+ };
12
+
13
+ /**
14
+ * Create an array of the attributes on an element
15
+ * @param {NamedNodeMap} attributes The attributes on an element
16
+ * @return {Array} The attributes on an element as an array of key/value pairs
17
+ */
18
+ const getAttributes = function (attributes) {
19
+ return Array.prototype.map.call(attributes, function (attribute) {
20
+ return {
21
+ attributeName: attribute.name,
22
+ value: attribute.value,
23
+ };
24
+ });
25
+ };
26
+
27
+ /**
28
+ * Create a DOM Tree Map for an element
29
+ *
30
+ * @param {HTMLElement} element The element to map
31
+ * @param {Boolean} isSVG If true, node is within an SVG
32
+ * @return {{ domMap: Array, plainlySetInnerHTML?: boolean, innerHTML?: string }} A DOM tree map
33
+ */
34
+ const createDOMMap = function (element, isChild = false, isSVG = false, type = 'template') {
35
+ const children = [...element.childNodes];
36
+
37
+ // If the list contains a comment which says "$$plainly-set-inner-html" it means
38
+ // that the all children of the element will be set as unsafe `innerHTML´
39
+ // which means that there won't be any diffing or fancy things
40
+ // but we will just mark it as a pure component and leave it be.
41
+ for (const child of children) {
42
+ if (child?.nodeType === NODE_TYPE_COMMENT && child.textContent === '$$plainly-set-inner-html') {
43
+ return {
44
+ domMap: [],
45
+ plainlySetInnerHTML: true,
46
+ innerHTML: element.innerHTML,
47
+ };
48
+ }
49
+ }
50
+
51
+ const domMap = children.map((node, index) => {
52
+ const details = {
53
+ content: node.childNodes && node.childNodes.length > 0 ? null : node.textContent,
54
+ attributes: node.nodeType !== 1 ? [] : getAttributes(node.attributes),
55
+ type:
56
+ node.nodeType === NODE_TYPE_TEXT
57
+ ? 'text'
58
+ : node.nodeType === NODE_TYPE_COMMENT
59
+ ? 'comment'
60
+ : node.tagName.toLowerCase(),
61
+ node: node,
62
+ };
63
+
64
+ details.isSVG = isSVG || details.type === 'svg';
65
+ details.children =
66
+ isChild && isTemplateElement({ node }) ? { domMap: [] } : createDOMMap(node, true, details.isSVG);
67
+ return details;
68
+ });
69
+
70
+ return {
71
+ domMap,
72
+ };
73
+ };
74
+
75
+ const removeAttributes = function (element, attributes) {
76
+ attributes.forEach(function (attribute) {
77
+ element.removeAttribute(attribute.attributeName);
78
+ });
79
+ };
80
+
81
+ /**
82
+ * Add attributes to an element
83
+ * @param {HTMLElement} element The element
84
+ * @param {[{ attributeName: string, value: string}]} attributes The attributes to add
85
+ */
86
+ const addAttributes = function (element, attributes) {
87
+ attributes.forEach(function (attribute) {
88
+ if (attribute.attributeName.startsWith('.')) {
89
+ // Directly set the attribute on the element instance as property
90
+ // instead of setting the attribute.
91
+ element[attribute.attributeName.substring(1)] = attribute.value || '';
92
+ return;
93
+ }
94
+
95
+ element.setAttribute(attribute.attributeName, attribute.value || '');
96
+ });
97
+ };
98
+
99
+ /**
100
+ * Diff the attributes on an existing element versus the template
101
+ * @param {Object} template The new template
102
+ * @param {Object} existing The existing DOM node
103
+ */
104
+ const diffAttributes = function (template, existing) {
105
+ // Get attributes to remove
106
+ const remove = existing.attributes.filter(function (attribute) {
107
+ const getAtt = template.attributes.find(function (newAtt) {
108
+ return attribute.attributeName === newAtt.attributeName;
109
+ });
110
+ return getAtt === undefined;
111
+ });
112
+
113
+ // Get attributes to change
114
+ const change = template.attributes.filter(function (attribute) {
115
+ const getAtt = existing.attributes.find((existingAtt) => {
116
+ return attribute.attributeName === existingAtt.attributeName;
117
+ });
118
+ return getAtt === undefined || getAtt.value !== attribute.value;
119
+ });
120
+
121
+ // Add/remove any required attributes
122
+ addAttributes(existing.node, change);
123
+ removeAttributes(existing.node, remove);
124
+ };
125
+
126
+ /**
127
+ * Make an HTML element
128
+ * @param {Object} element The element details
129
+ * @param {Object} templateResult
130
+ * @return {HTMLElement|false} The HTML element
131
+ */
132
+ const makeElement = function (element, templateResult) {
133
+ // Create the element
134
+ let node;
135
+ if (element.type === 'text') {
136
+ node = document.createTextNode(element.content);
137
+ } else if (element.type === 'comment') {
138
+ node = document.createComment(element.content);
139
+ } else if (element.isSVG) {
140
+ node = document.createElementNS('http://www.w3.org/2000/svg', element.type);
141
+ } else {
142
+ node = document.createElement(element.type);
143
+ }
144
+
145
+ // Add attributes
146
+ addAttributes(node, element.attributes);
147
+
148
+ // If the element has child nodes, create them
149
+ // Otherwise, add textContent
150
+
151
+ if (element.children.domMap.length > 0) {
152
+ diff(element.children, { domMap: [] }, node);
153
+ } else if (templateResult.plainlySetInnerHTML) {
154
+ node.innerHTML = templateResult.innerHTML;
155
+ } else if (element.type !== 'text') {
156
+ node.textContent = element.content;
157
+ }
158
+
159
+ return node;
160
+ };
161
+
162
+ const isTemplateElement = (element) => {
163
+ const tagName = element.node?.tagName?.toLowerCase() ?? false;
164
+ if (!tagName || !tagName.includes('-')) {
165
+ return false;
166
+ }
167
+
168
+ if (_cachedTemplateElements[tagName]) {
169
+ return true;
170
+ }
171
+
172
+ const elementClass = customElements.get(tagName);
173
+ const isTemplateElement = elementClass && elementClass._$templateElement$ === true;
174
+
175
+ if (isTemplateElement) {
176
+ _cachedTemplateElements[tagName] = elementClass;
177
+ }
178
+
179
+ return isTemplateElement;
180
+ };
181
+
182
+ /**
183
+ * Diff the existing DOM node versus the template
184
+ * @param {{ domMap: Array, plainlySetInnerHTML?: boolean, innerHTML?: string }} templateResult A DOM tree map of the template content
185
+ * @param {{ domMap: array }} domResult A DOM tree map of the existing DOM node
186
+ * @param {HTMLElement} element The element to render content into
187
+ */
188
+ const diff = function (templateResult, domResult, element) {
189
+ const templateMap = templateResult.domMap;
190
+
191
+ if (templateResult.plainlySetInnerHTML) {
192
+ element.innerHTML = templateResult.innerHTML;
193
+ return;
194
+ }
195
+
196
+ const domMap = domResult.domMap;
197
+
198
+ // If extra elements in domMap, remove them
199
+ let count = domMap.length - templateMap.length;
200
+ if (count > 0) {
201
+ for (; count > 0; count--) {
202
+ domMap[domMap.length - count].node.parentNode.removeChild(domMap[domMap.length - count].node);
203
+ }
204
+ }
205
+
206
+ // Diff each item in the templateMap
207
+ templateMap.forEach(function (node, index) {
208
+ // If element doesn't exist, create it
209
+ if (!domMap[index]) {
210
+ const newElement = makeElement(node, node.children);
211
+ element.appendChild(newElement);
212
+ return;
213
+ }
214
+
215
+ // If element is not the same type, replace it with new element
216
+ if (templateMap[index].type !== domMap[index].type) {
217
+ domMap[index].node.parentNode.replaceChild(
218
+ makeElement(templateMap[index], node.children),
219
+ domMap[index].node,
220
+ );
221
+ return;
222
+ }
223
+
224
+ // If attributes are different, update them
225
+ diffAttributes(templateMap[index], domMap[index]);
226
+
227
+ if (node.children.plainlySetInnerHTML && domMap[index]) {
228
+ domMap[index].innerHTML = node.children.innerHTML;
229
+ return;
230
+ }
231
+
232
+ // If content is different, update it
233
+ if (templateMap[index].content !== domMap[index].content) {
234
+ if (!isTemplateElement(domMap[index])) {
235
+ // ignore custom elements.
236
+ domMap[index].node.textContent = templateMap[index].content;
237
+ }
238
+ }
239
+
240
+ // If target element should be empty, wipe it
241
+ if (domMap[index].children.domMap.length > 0 && node.children.domMap.length < 1) {
242
+ if (!isTemplateElement(domMap[index])) {
243
+ // ignore custom elements.
244
+ domMap[index].node.innerHTML = '';
245
+ }
246
+ return;
247
+ }
248
+
249
+ // If element is empty and shouldn't be, build it up
250
+ // This uses a document fragment to minimize reflows
251
+ if (domMap[index].children.domMap.length === 0 && node.children.domMap.length > 0) {
252
+ if (isTemplateElement(domMap[index])) {
253
+ return;
254
+ }
255
+
256
+ const fragment = document.createDocumentFragment();
257
+ diff(node.children, domMap[index].children, domMap[index].node);
258
+ element.appendChild(fragment);
259
+ return;
260
+ }
261
+
262
+ // If there are existing child elements that need to be modified, diff them
263
+ if (node.children.domMap.length > 0) {
264
+ if (!isTemplateElement(node)) {
265
+ diff(node.children, domMap[index].children, domMap[index].node);
266
+ }
267
+ }
268
+ });
269
+ };
270
+
271
+ const render = (template, target) => {
272
+ const templateDOMMap = createDOMMap(convertStringToHTML(template), false, false, 'template');
273
+ const domMap = createDOMMap(target, false, false, 'dom');
274
+
275
+ diff(templateDOMMap, domMap, target);
276
+ };
277
+
278
+ export { render };
@@ -64,3 +64,21 @@ export function dashToCamel(string) {
64
64
  export function camelToDash(string) {
65
65
  return string.replace(/([A-Z])/g, '-$1').toLowerCase();
66
66
  }
67
+
68
+ export function decodeAttribute(attribute) {
69
+ return `${attribute}`
70
+ .replace(/&lt;/g, '<')
71
+ .replace(/&gt;/g, '>')
72
+ .replace(/&quot;/g, '"')
73
+ .replace(/&apos;/g, "'");
74
+ }
75
+
76
+ export function encodeAttribute(attribute) {
77
+ return `${attribute}`
78
+ .replace(/'/g, '&apos;')
79
+ .replace(/"/g, '&quot;')
80
+ .replace(/</g, '&lt;')
81
+ .replace(/>/g, '&gt;')
82
+ .replace(/\r\n/g, '\n')
83
+ .replace(/[\r\n]/g, '\n');
84
+ }