@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.
@@ -1,3 +1,35 @@
1
+ /* An Element node like <p> or <div> */
2
+ export const ELEMENT_NODE = 1;
3
+ /* An Attribute of an Element */
4
+ export const ATTRIBUTE_NODE = 2;
5
+ /* The actual Text inside an Element or Attr */
6
+ export const TEXT_NODE = 3;
7
+ /* A Comment node, such as <!-- … --> */
8
+ export const COMMENT_NODE = 8;
9
+ /* A Document Fragment, a document without a parent */
10
+ export const DOCUMENT_FRAGMENT_NODE = 11;
11
+
12
+ /**
13
+ * Gets a list of (childNode) indexes for the given node
14
+ * @param {Node} node
15
+ * @return {number[]}
16
+ */
17
+ export const getNodePath = (node) => {
18
+ const path = [];
19
+ let parentNode = node.parentNode;
20
+ while (parentNode) {
21
+ path.push([...parentNode.childNodes].indexOf(node));
22
+ node = parentNode;
23
+ parentNode = node.parentNode;
24
+ }
25
+ return path;
26
+ };
27
+
28
+ /**
29
+ * Get a list of all parent nodes up the DOM tree
30
+ * @param {Node} domNode
31
+ * @returns {Node[]}
32
+ */
1
33
  export function getAllParentNodes(domNode) {
2
34
  let node = domNode.parentElement;
3
35
  let parents = [];
@@ -9,7 +41,12 @@ export function getAllParentNodes(domNode) {
9
41
  return parents;
10
42
  }
11
43
 
12
- // https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
44
+ /**
45
+ * Gets the parent/root element of a shadow root or the global document body if not in shadow root.
46
+ * https://stackoverflow.com/questions/27453617/how-can-i-tell-if-an-element-is-in-a-shadow-dom
47
+ * @param {Node} element
48
+ * @returns {ShadowRoot | HTMLElement}
49
+ */
13
50
  export function getShadowParentOrBody(element) {
14
51
  if (element instanceof ShadowRoot) {
15
52
  return element;
@@ -25,6 +62,11 @@ export function getShadowParentOrBody(element) {
25
62
 
26
63
  // TODO: add function for getClosestParentOfNodeType('custom-element')
27
64
 
65
+ /**
66
+ * Get the closest/first parent node that is a custom element
67
+ * @param {Node} domNode
68
+ * @returns {Node}
69
+ */
28
70
  export function getClosestParentCustomElementNode(domNode) {
29
71
  const customElementParents = getAllParentNodes(domNode).filter((node) => {
30
72
  return node.localName && node.localName.indexOf('-') !== -1;
@@ -33,14 +75,214 @@ export function getClosestParentCustomElementNode(domNode) {
33
75
  return customElementParents.pop();
34
76
  }
35
77
 
78
+ /**
79
+ * Checks whether two nodes are of the same node type
80
+ * @param {Node} nodeA
81
+ * @param {Node} nodeB
82
+ * @returns {boolean}
83
+ */
36
84
  export function isOfSameNodeType(nodeA, nodeB) {
37
85
  if (!nodeA && !nodeA.localName) return false;
38
86
  if (!nodeB && !nodeB.localName) return false;
39
87
  return nodeA.localName === nodeB.localName;
40
88
  }
41
89
 
42
- export const supportsAdoptingStyleSheets = () =>
43
- 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
90
+ /**
91
+ * Checks if adopting style sheets is supported in browser
92
+ * @returns {boolean}
93
+ */
94
+ export const supportsAdoptingStyleSheets = () => {
95
+ return 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
96
+ };
97
+
98
+ /**
99
+ * Better implementation of hasChildNodes check because the native check will also count for empty whitespace as child nodes... :(
100
+ * @param {Node} node
101
+ * @returns {boolean}
102
+ */
103
+ export const hasChildNodes = (node) => {
104
+ const childNodesLength = node.childNodes.length;
105
+
106
+ if (childNodesLength === 0) return false;
107
+
108
+ if (childNodesLength === 1 && node.firstChild.nodeType === 3 && node.firstChild.textContent.trim() === '')
109
+ return false;
110
+
111
+ // children will not contain text nodes...
112
+ if (childNodesLength > 0) return true;
113
+
114
+ // TODO: check if more than 1 child nodes, but all childnodes are whitespace text ndoes?!
115
+ };
116
+
117
+ /**
118
+ * Parses the given string as HTML template and returns a real DOM element
119
+ * @param {string} template
120
+ * @returns {HTMLElement}
121
+ */
122
+ export const convertStringToHTML = (template) => {
123
+ const parser = new DOMParser();
124
+ const document = parser.parseFromString(template.toString(), 'text/html');
125
+ return document.body;
126
+ };
127
+
128
+ /**
129
+ * Parses the given string and returns a DocumentFragment which contains the DOM subtree
130
+ * representing the <template> element's template contents.
131
+ * @param {string} string
132
+ * @return {DocumentFragment}
133
+ */
134
+ export const convertStringToTemplate = (string) => {
135
+ const template = globalThis.document?.createElement('template');
136
+ // TODO: ensure that SSRed and CSRed templates have the same amount of whitespace text nodes
137
+ template.innerHTML = string.trimStart().trimEnd();
138
+ return template.content;
139
+ };
140
+
141
+ /**
142
+ * Diff the attributes for a live DOM element against a template DOM element
143
+ * @param {Element} templateElement - The new template
144
+ * @param {Element} domElement - The existing DOM node
145
+ */
146
+ export const diffAttributes = function (templateElement, domElement) {
147
+ // remove or update previous attributes
148
+ for (/** @type {Attr} */ const attribute of Array.from(domElement.attributes)) {
149
+ if (templateElement.hasAttribute(attribute.name)) {
150
+ const oldValue = attribute.value;
151
+ const newValue = templateElement.getAttribute(attribute.name) || '';
152
+ if (oldValue !== newValue) {
153
+ domElement.setAttribute(attribute.name, newValue);
154
+ }
155
+ } else {
156
+ domElement.removeAttribute(attribute.name);
157
+ }
158
+ }
159
+
160
+ // set newly added attributes
161
+ for (/** @type {Attr} */ const attribute of Array.from(templateElement.attributes)) {
162
+ if (!domElement.hasAttribute(attribute.name)) {
163
+ domElement.setAttribute(attribute.name, attribute.value);
164
+ }
165
+ }
166
+ };
167
+
168
+ /**
169
+ * Diff a live DOM node against a template DOM node
170
+ * @param {Node} templateNode
171
+ * @param {Node} domNode
172
+ * @param {(Node) => Boolean} shouldDiffChildNodes
173
+ */
174
+ export const diffNodes = function (templateNode, domNode, shouldDiffChildNodes = (templateChildNode) => true) {
175
+ // If the DOM node should be empty, remove all child nodes
176
+ if (hasChildNodes(domNode) && !hasChildNodes(templateNode)) {
177
+ domNode.replaceChildren();
178
+ return;
179
+ }
180
+
181
+ // If the DOM node is empty, but should have children, add child nodes from the template node
182
+ if (!hasChildNodes(domNode) && hasChildNodes(templateNode)) {
183
+ domNode.replaceChildren(...templateNode.childNodes);
184
+ return;
185
+ }
186
+
187
+ const templateChildNodes = [...templateNode.childNodes];
188
+ const domChildNodes = [...domNode.childNodes];
189
+
190
+ // Diff each node in the child node lists
191
+ let length = Math.max(templateChildNodes.length, domChildNodes.length);
192
+ for (let index = 0; index < length; index++) {
193
+ const templateChildNode = templateChildNodes[index];
194
+ const domChildNode = domChildNodes[index];
195
+
196
+ // If the DOM node doesn't exist, append/copy the template node
197
+ if (!domChildNode) {
198
+ domNode.appendChild(templateChildNode);
199
+ continue;
200
+ }
201
+
202
+ // If the template node doesn't exist, remove the node in the DOM
203
+ if (!templateChildNode) {
204
+ domNode.removeChild(domChildNode);
205
+ continue;
206
+ }
207
+
208
+ // If DOM node is equal to the template node, don't do anything
209
+ if (domChildNode.isEqualNode(templateChildNode)) {
210
+ continue;
211
+ }
212
+
213
+ // the following two checks don't have to be correct, but it could bring as back to the fast path
214
+ // in the worst case we still have to compare and swap all the elements
215
+ // but in the best case we find the one element that should actually be inserted or removed
216
+ // and from there on, every node that comes after should be equal again
217
+
218
+ // fast path for removing DOM nodes - we delete the node now instead of at the end
219
+ if (domNode.childNodes.length > templateNode.childNodes.length) {
220
+ domChildNodes.splice(index, 1);
221
+ domNode.removeChild(domChildNode);
222
+ // because domChildNodes will get shorter by splicing it, everything moves up by one
223
+ // so the (actual) next element will have the same index now as we currently have
224
+ // therefore we have to adjust our counters
225
+ length--;
226
+ index--;
227
+
228
+ // we might have corrected everything and the parent nodes could be equal again
229
+ // if so, we can skip the rest of the child node checks
230
+ if (domNode.childNodes.length === templateNode.childNodes.length && domNode.isEqualNode(templateNode)) {
231
+ return;
232
+ }
233
+
234
+ continue;
235
+ }
236
+
237
+ // fast path for adding DOM nodes - we insert the node now instead of at the end
238
+ // but NOT if we already are at the end of the list of child nodes
239
+ if (index !== domNode.childNodes.length - 1 && domNode.childNodes.length < templateNode.childNodes.length) {
240
+ domChildNodes.splice(index, 0, templateChildNode);
241
+ domNode.insertBefore(templateChildNode, domChildNode);
242
+
243
+ // we might have corrected everything and the parent nodes could be equal again
244
+ // if so, we can skip the rest of the child node checks
245
+ if (domNode.childNodes.length === templateNode.childNodes.length && domNode.isEqualNode(templateNode)) {
246
+ return;
247
+ }
248
+
249
+ continue;
250
+ }
44
251
 
45
- // for IE11 we are using the ShadyDOM Polyfill. With the polyfill we cannot append stylesheets to the shadowRoot
46
- export const supportsAppendingStyleSheets = !window.ShadyDOM;
252
+ // If node types are not the same, replace the DOM node with the template node
253
+ if (templateChildNode.nodeType !== domChildNode.nodeType) {
254
+ domNode.replaceChild(templateChildNode, domChildNode);
255
+ continue;
256
+ }
257
+
258
+ // If the node is a Text node or a Comment node, update it
259
+ if (domChildNode.nodeType === TEXT_NODE || domChildNode.nodeType === COMMENT_NODE) {
260
+ domChildNode.textContent = templateChildNode.textContent;
261
+ continue;
262
+ }
263
+
264
+ // If the element tag names are not the same, replace the DOM node with the template node
265
+ if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName !== domChildNode.tagName) {
266
+ domNode.replaceChild(templateChildNode, domChildNode);
267
+ continue;
268
+ }
269
+
270
+ // If the node is an SVG element, don't even think about diffing it, just replace it
271
+ if (templateChildNode.nodeType === ELEMENT_NODE && templateChildNode.tagName === 'SVG') {
272
+ domNode.replaceChild(templateChildNode, domChildNode);
273
+ continue;
274
+ }
275
+
276
+ // If the node is an Element node, diff the attributes
277
+ if (templateChildNode.nodeType === ELEMENT_NODE) {
278
+ diffAttributes(templateChildNode, domChildNode);
279
+ }
280
+
281
+ // If there are child nodes, diff them recursively
282
+ if (templateChildNode.hasChildNodes()) {
283
+ if (shouldDiffChildNodes(templateChildNode)) {
284
+ diffNodes(templateChildNode, domChildNode);
285
+ }
286
+ }
287
+ }
288
+ };
@@ -0,0 +1,35 @@
1
+ export class Directive {
2
+ /** @type {Element} */
3
+ node = undefined;
4
+
5
+ /**
6
+ * @param {Node} node
7
+ */
8
+ constructor(node) {
9
+ this.node = node;
10
+ }
11
+
12
+ /**
13
+ * @abstract
14
+ */
15
+ update() {}
16
+
17
+ // TODO: this is somewhat custom... maybe we could set the initial values in the constructor and then simply override the default toString() method?!
18
+ /**
19
+ * Implement the stringify method and return a string for rendering the directive in SSR mode
20
+ * @param {...any} values
21
+ * @returns {string}
22
+ */
23
+ stringify(...values) {
24
+ return '';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @param {Class<Directive>} directiveClass
30
+ */
31
+ export const defineDirective = (directiveClass) => {
32
+ return (...values) => {
33
+ return { directiveClass, values };
34
+ };
35
+ };
package/src/util/Store.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { isObjectLike, deepEquals } from './AttributeParser.js';
2
- import { BaseElement } from '../BaseElement';
2
+ import { BaseElement } from '../BaseElement.js';
3
3
 
4
4
  export class Store {
5
5
  _observer = new Set();
6
6
  _singlePropertyMode = false;
7
7
  _state = {};
8
8
 
9
+ /**
10
+ * @param {* | object} value
11
+ */
9
12
  constructor(value) {
10
13
  // wrap primitives with a generic "value" field to generate getter and setter
11
14
  this._singlePropertyMode = arguments.length > 0 && !isObjectLike(value);
@@ -33,6 +36,11 @@ export class Store {
33
36
  });
34
37
  }
35
38
 
39
+ /**
40
+ * Properties to be defined and assigned on the store. Can be overridden in extended classes.
41
+ * Properties defined here will also trigger an update cycle when changed.
42
+ * @return {{}}
43
+ */
36
44
  properties() {
37
45
  return {};
38
46
  }
@@ -40,34 +48,47 @@ export class Store {
40
48
  /**
41
49
  * When a property as the key was changed in the store, the callback function defined as value
42
50
  * will be called with `newValue` and `oldValue`
43
- * eg. { property: (oldValue, newValue) => { console.log('property changed!, oldValue, newValue); } }
44
51
  * @return {{}}
45
52
  */
46
53
  watch() {
47
54
  return {};
48
55
  }
49
56
 
57
+ /**
58
+ * Overrides `valueOf` to automatically return .value
59
+ * @returns {* | {}}
60
+ */
50
61
  valueOf() {
51
62
  return this._singlePropertyMode ? this._state.value : this._state;
52
63
  }
53
64
 
65
+ /**
66
+ * Overrides `toString` to automatically return .value
67
+ * @returns {string}
68
+ */
54
69
  toString() {
55
70
  return `${this._singlePropertyMode ? this._state.value : this._state}`;
56
71
  }
57
72
 
58
73
  /**
59
- * @param {BaseElement|Function} observer
74
+ * Add an observer
75
+ * @param {BaseElement | Function} observer
60
76
  */
61
77
  subscribe(observer) {
62
78
  this._observer.add(observer);
63
79
  }
80
+
64
81
  /**
65
- * @param {BaseElement|Function} observer
82
+ * Remove an observer
83
+ * @param {BaseElement | Function} observer
66
84
  */
67
85
  unsubscribe(observer) {
68
86
  this._observer.delete(observer);
69
87
  }
70
88
 
89
+ /**
90
+ * Request and batch an asynchronous update cycle
91
+ */
71
92
  requestUpdate() {
72
93
  this._observer.forEach(async (observer) => {
73
94
  if (observer instanceof BaseElement) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Wrapper for defining custom elements so that registering an element multiple times won't crash
3
- * @param name for the tag
4
- * @param constructor for the custom element
3
+ * @param {string} name - name for the element tag
4
+ * @param {CustomElementConstructor} constructor for the custom element
5
5
  */
6
6
  export function defineElement(name, constructor) {
7
7
  try {
package/src/util/i18n.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Retrieves a translated key from a dictionary on the window object
3
3
  * Example: ${i18n('CustomElement.buttonLabel', 'Label')}
4
- * @param key to be translated
5
- * @param fallback to be used if key is not defined
6
- * @return String of the translated key or fallback or original key
4
+ * @param {string} key - to be translated
5
+ * @param {string} fallback - to be used if key is not defined
6
+ * @return {string} - String of the translated key or fallback or original key
7
7
  */
8
8
  export function i18n(key, fallback) {
9
9
  try {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Little wrapper function for JSON.stringify() to easily convert objects and arrays to strings
3
3
  * to be able to set them as attributes on custom elements
4
- * @param value to be stringified
5
- * @return String of the stringified value
4
+ * @param {object | array} value - to be stringified
5
+ * @return {string}
6
6
  */
7
7
  export function toString(value) {
8
8
  try {
package/CHANGELOG.md DELETED
@@ -1,142 +0,0 @@
1
- # Change Log
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
6
-
7
- <!--
8
- PRs should document their user-visible changes (if any) in the
9
- Unreleased section, uncommenting the header as necessary.
10
- -->
11
-
12
- <!-- ## Unreleased -->
13
- <!-- ### Added -->
14
- <!-- ### Changed -->
15
- <!-- ### Removed -->
16
- <!-- ### Fixed -->
17
-
18
- ## [0.7.3] - 2023-03-07
19
-
20
- ### Fixed
21
- * last release was tagged wrong
22
-
23
- ## [0.7.2] - 2023-03-03
24
-
25
- ### Fixed
26
- * watcher callbacks for injected stores
27
-
28
- ## [0.7.1] - 2023-03-03
29
-
30
- ### Added
31
- * Elements are now able to watch Stores
32
-
33
- ### Fixed
34
- * Injection of falsy values
35
-
36
- ## [0.7.0] - 2023-02-13
37
-
38
- ### Added
39
- * Dependency Injection via Context Protocol 💉
40
- * Provide reactive Properties or entire Stores via: provideProperties() { return { name : value}}
41
- * Inject / Request them via injectProperties() { return { name : 'defaultValue'}}
42
-
43
- ### Changed
44
- * docs structure
45
-
46
- ### Fixed
47
- * Cannot re-render slotted light dom when using the vanilla renderer https://github.com/webtides/element-js/issues/76
48
- * adds Vanilla Renderer Tests
49
-
50
-
51
- ## [0.6.2] - 2022-11-18
52
-
53
- ### FIXED
54
- * Store always switching to singleProperty Mode if constructed without any arguments.
55
-
56
- ## [0.6.1] - 2022-11-18
57
-
58
- ### ADDED
59
- * Store watch() map to control internal store state scope
60
-
61
- ## [0.6.0] - 2022-11-17
62
-
63
- ### ADDED
64
- * StoreProperty for shared application state
65
- * Directives for Vanilla Renderer
66
-
67
- ## [0.5.0] - 2022-09-13
68
-
69
- ### Changed
70
- * improved the performance of the vanilla-renderer
71
-
72
- ### Removed
73
- * setting attributes as properties during dom diffing for the vanilla renderer. This is potentially a **breaking** change if you used attributes with the "." (dot) notation
74
-
75
- ## [0.4.3] - 2022-07-06
76
-
77
- ### ADDED
78
- * parse option to propertyOptions that can be either a boolean OR a function for custom parsing
79
- * enhance reflect option in propertyOptions that it can also be a function for custom reflection
80
-
81
- ## [0.4.2] - 2022-02-03
82
-
83
- ### Fixed
84
- * vanilla-renderer: fixes attribute comparsion
85
-
86
- ## [0.4.1] - 2022-02-01
87
-
88
- ### Fixed
89
- * refs="id[]" indexing keys not nodes *rolleyes*
90
-
91
- ### Added
92
- * refs="id[]" aka possibbility to refrence a list of nodes to the $refs map.
93
-
94
- ## [0.4.0] - 2022-01-28
95
-
96
- ### Added
97
- * a "vanilla" renderer with support for hydration and no dependencies
98
-
99
- ### Changed
100
- * uses new @web/test-runner isntead of karma (#27)
101
-
102
- ## [0.3.3] - 2021-09-22
103
-
104
- ### Added
105
- * optional complex event callback notation to support addEventListener options (i.E. for passive Event Listeners )
106
- * Example: {listener: () => {}, options: { passive: true } }
107
- * docs
108
- * tests
109
-
110
-
111
- ## [0.3.2] - 2021-03-09
112
-
113
- ### Added
114
- * bundled version of `element-js` to use from a CDN and be able to prototype in tools like CodePen etc.
115
-
116
- ## [0.3.1] - 2020-12-08
117
-
118
- ### Fixed
119
- * i18n fallback values
120
-
121
- ### Added
122
- * i18n tests
123
-
124
- ## [0.3.0] - 2020-07-27
125
-
126
- ### Added
127
- * first batch/draft of the documentation
128
- * property/attribute reflection via constructor options (see the [Documentation](/docs/README.md#propertyoptions) for more info)
129
- * mutationObserverOptions to enable observing subtree mutations (see the [Documentation](/docs/README.md#mutationobserveroptions) for more info)
130
-
131
- ### Removed
132
- * `childListUpdate` constructor option has been deprecated and will be removed before `1.0`
133
-
134
- ## [0.2.0] - 2020-07-01
135
-
136
- ### Changed
137
- * uses shady-render instead of render from `lit-html` for rendering templates to be compatible with the shady-css polyfill
138
- * refactored and simplified `StyledElement`s handling/loading of style sheets
139
-
140
- ## [0.1.0] - 2020-06-26
141
-
142
- * initial release
@@ -1,50 +0,0 @@
1
- import { StyledElement } from '../../StyledElement.js';
2
- import { html, spreadAttributes, unsafeHTML } from './util/html.js';
3
- import { render } from './util/render.js';
4
- export { i18n } from '../../util/i18n.js';
5
-
6
- class TemplateElement extends StyledElement {
7
- constructor(options) {
8
- super({
9
- deferUpdate: false,
10
- shadowRender: false,
11
- styles: [],
12
- adoptGlobalStyles: true,
13
- mutationObserverOptions: {
14
- childList: false,
15
- },
16
- ...options,
17
- });
18
- this._template = this._options.template;
19
-
20
- if (this._options.shadowRender && !this.shadowRoot) {
21
- this.attachShadow({ mode: 'open' });
22
- }
23
- }
24
-
25
- template() {
26
- return html``;
27
- }
28
-
29
- update(options) {
30
- this.renderTemplate();
31
- super.update(options);
32
- }
33
-
34
- renderTemplate() {
35
- const template = this._template || this.template();
36
-
37
- if (typeof template === 'string') {
38
- // just a plain string literal. no fancy rendering required
39
- this.getRoot().innerHTML = `${template}`;
40
- } else {
41
- render(template, this.getRoot());
42
- }
43
- }
44
-
45
- getRoot() {
46
- return this.shadowRoot !== null ? this.shadowRoot : this;
47
- }
48
- }
49
-
50
- export { TemplateElement, html, unsafeHTML, render, spreadAttributes };
@@ -1,18 +0,0 @@
1
- import { BaseElement } from '../../BaseElement.js';
2
- import { StyledElement } from '../../StyledElement.js';
3
- import { TemplateElement, html, unsafeHTML, spreadAttributes } from './TemplateElement.js';
4
- import { toString } from '../../util/toString.js';
5
- import { defineElement } from '../../util/defineElement.js';
6
- import { i18n } from '../../util/i18n.js';
7
-
8
- export {
9
- BaseElement,
10
- StyledElement,
11
- TemplateElement,
12
- html,
13
- unsafeHTML,
14
- spreadAttributes,
15
- toString,
16
- defineElement,
17
- i18n,
18
- };