mi-element 0.9.2 → 0.9.4

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/dist/html.js CHANGED
@@ -2,21 +2,26 @@ import { toJson } from './utils.js';
2
2
 
3
3
  const globalRenderCache = new class {
4
4
  cnt=0;
5
- map=new Map;
6
- cache=new WeakMap;
5
+ cache=new Map;
6
+ last=0;
7
+ get size() {
8
+ return this.cache.size;
9
+ }
7
10
  _inc() {
8
11
  return this.cnt = 268435455 & ++this.cnt, this.cnt;
9
12
  }
10
13
  clear() {
11
- this.cnt = 0, this.map.clear();
14
+ this.cnt = 0, this.cache.clear();
12
15
  }
13
16
  set(value) {
14
- const key = '__rc:' + this._inc().toString(36), ref = {};
15
- return this.map.set(key, ref), this.cache.set(ref, value), key;
17
+ const now = Date.now();
18
+ this.last < now && this.cache.clear(), this.last = now + 5e3;
19
+ const key = '__rc:' + this._inc().toString(36);
20
+ return this.cache.set(key, value), key;
16
21
  }
17
22
  get(key) {
18
- const ref = this.map.get(key);
19
- return this.map.delete(key), this.cache.get(ref);
23
+ const value = this.cache.get(key);
24
+ return this.cache.delete(key), value;
20
25
  }
21
26
  };
22
27
 
@@ -28,9 +33,9 @@ const unsafeHtml = str => new UnsafeHtml(str), escMap = {
28
33
  '>': '&gt;',
29
34
  '"': '&quot;',
30
35
  "'": '&#39;'
31
- }, esc = string => string.replace(/[&<>"']/g, tag => escMap[tag]), escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string)), escValue = any => {
36
+ }, esc = string => string.replace(/[&<>"']/g, tag => escMap[tag]), escHtml = string => string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string)), OBJECT = 'object', FUNCTION = 'function', escValue = any => {
32
37
  if (any instanceof UnsafeHtml) return any;
33
- if ([ 'object', 'function' ].includes(typeof any)) {
38
+ if ([ OBJECT, FUNCTION ].includes(typeof any)) {
34
39
  const key = globalRenderCache.set(any);
35
40
  return unsafeHtml(key);
36
41
  }
@@ -47,26 +52,38 @@ function render(node, template, handlers = {}) {
47
52
  return refs;
48
53
  }
49
54
 
55
+ const REF = 'ref', REF_Q = '[ref]';
56
+
50
57
  function renderAttrs(node, handlers = {}, refs = {}) {
51
- if (node.nodeType === Node.ELEMENT_NODE) for (let attr of node.attributes) {
52
- const startsWith = attr.name[0], name = attr.name.slice(1);
53
- let rm = 0;
54
- if ('?' === startsWith) toJson(attr.value) ? node.setAttribute(name, '') : node.removeAttribute(name),
55
- rm = 1; else if ('...' === attr.name) {
56
- const obj = globalRenderCache.get(attr.value);
57
- if (obj && 'object' == typeof obj) for (const [k, v] of Object.entries(obj)) node[k] = v;
58
- rm = 1;
59
- } else if ('.' === startsWith) node[name] = globalRenderCache.get(attr.value) ?? attr.value,
60
- rm = 1; else if ('@' === startsWith) {
61
- const handlerName = attr.value, fn = globalRenderCache.get(handlerName);
62
- fn ? node.addEventListener(name, e => fn(e)) : 'function' == typeof handlers[handlerName] && node.addEventListener(name, e => handlers[handlerName](e)),
63
- rm = 1;
64
- } else 'ref' === attr.name && (refs[attr.value] = node, rm = 1);
65
- rm && requestAnimationFrame(() => {
66
- node.removeAttribute(attr.name);
67
- });
58
+ if (node.nodeType === Node.ELEMENT_NODE) {
59
+ const rmFns = [];
60
+ for (let attr of node.attributes) {
61
+ const startsWith = attr.name[0], name = attr.name.slice(1);
62
+ let rm = 0;
63
+ if ('?' === startsWith) toJson(attr.value) ? node.setAttribute(name, '') : node.removeAttribute(name),
64
+ rm = 1; else if ('...' === attr.name) {
65
+ const obj = globalRenderCache.get(attr.value);
66
+ if (obj && typeof obj === OBJECT) for (const [k, v] of Object.entries(obj)) node[k] = v;
67
+ rm = 1;
68
+ } else if ('.' === startsWith) node[name] = globalRenderCache.get(attr.value) ?? attr.value,
69
+ rm = 1; else if ('@' === startsWith) {
70
+ const handlerName = attr.value, fn = globalRenderCache.get(handlerName);
71
+ fn ? node.addEventListener(name, e => fn(e)) : typeof handlers[handlerName] === FUNCTION && node.addEventListener(name, e => handlers[handlerName](e)),
72
+ rm = 1;
73
+ } else attr.name === REF && (refs[attr.value] = node, rm = 1);
74
+ rm && rmFns.push([ node, attr.name ]);
75
+ }
76
+ rmFns.forEach(([node, name]) => node.removeAttribute(name));
77
+ }
78
+ if (customElements.get(node.localName)) {
79
+ const q = node.querySelectorAll(REF_Q);
80
+ for (let el of q) {
81
+ const refName = el.getAttribute(REF);
82
+ refName && !refs[refName] && (refs[refName] = el);
83
+ }
84
+ return refs;
68
85
  }
69
- if (!node.children?.length || customElements.get(node.localName)) return refs;
86
+ if (!node.children?.length) return refs;
70
87
  for (let child of Array.from(node.children)) renderAttrs(child, handlers, refs);
71
88
  return refs;
72
89
  }
package/dist/index.js CHANGED
@@ -8,6 +8,6 @@ export { refsBySelector } from './refs.js';
8
8
 
9
9
  export { Store } from './store.js';
10
10
 
11
- export { addGlobalStyles, classNames, css, styleMap } from './styling.js';
11
+ export { addGlobalStyles, classNames, css, escCss, styleMap, unsafeCss } from './styling.js';
12
12
 
13
13
  export { default as Signal } from 'mi-signal';
package/dist/styling.js CHANGED
@@ -3,7 +3,7 @@ import { camelToKebabCase } from './case.js';
3
3
  const classNames = (...args) => {
4
4
  const classList = [];
5
5
  return args.forEach(arg => {
6
- arg && ('string' == typeof arg ? classList.push(arg) : Array.isArray(arg) ? classList.push(classNames(...arg)) : 'object' == typeof arg && Object.entries(arg).forEach(([key, value]) => {
6
+ arg && ('string' == typeof arg ? classList.push(arg) : 'object' == typeof arg && Object.entries(arg).forEach(([key, value]) => {
7
7
  value && classList.push(key);
8
8
  }));
9
9
  }), classList.join(' ');
@@ -26,8 +26,14 @@ function addGlobalStyles(renderRoot) {
26
26
  })), globalSheets));
27
27
  }
28
28
 
29
- const css = (strings, ...values) => String.raw({
29
+ class UnsafeCss extends String {}
30
+
31
+ const unsafeCss = str => new UnsafeCss(str), escMap = {
32
+ '&': '\\26 ',
33
+ '<': '\\3c ',
34
+ '>': '\\3e '
35
+ }, escCss = string => string instanceof UnsafeCss ? string : unsafeCss((string => string.replace(/[&<>]/g, tag => escMap[tag]))('' + string)), css = (strings, ...values) => String.raw({
30
36
  raw: strings
31
- }, ...values);
37
+ }, ...values.map(escCss));
32
38
 
33
- export { addGlobalStyles, classNames, css, styleMap };
39
+ export { addGlobalStyles, classNames, css, escCss, styleMap, unsafeCss };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mi-element",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Build lightweight reactive micro web-components",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/commenthol/mi-element/tree/main/packages/mi-element#readme",
package/src/html.js CHANGED
@@ -5,8 +5,12 @@ import { toJson } from './utils.js'
5
5
  */
6
6
  class RenderCache {
7
7
  cnt = 0
8
- map = new Map()
9
- cache = new WeakMap()
8
+ cache = new Map()
9
+ last = 0
10
+
11
+ get size() {
12
+ return this.cache.size
13
+ }
10
14
 
11
15
  _inc() {
12
16
  this.cnt = ++this.cnt & 0xfffffff
@@ -15,21 +19,24 @@ class RenderCache {
15
19
 
16
20
  clear() {
17
21
  this.cnt = 0
18
- this.map.clear()
22
+ this.cache.clear()
19
23
  }
20
24
 
21
25
  set(value) {
26
+ const now = Date.now()
27
+ if (this.last < now) {
28
+ this.cache.clear()
29
+ }
30
+ this.last = now + 5e3
22
31
  const key = '__rc:' + this._inc().toString(36)
23
- const ref = {}
24
- this.map.set(key, ref)
25
- this.cache.set(ref, value)
32
+ this.cache.set(key, value)
26
33
  return key
27
34
  }
28
35
 
29
36
  get(key) {
30
- const ref = this.map.get(key)
31
- this.map.delete(key)
32
- return this.cache.get(ref)
37
+ const value = this.cache.get(key)
38
+ this.cache.delete(key)
39
+ return value
33
40
  }
34
41
  }
35
42
 
@@ -70,6 +77,9 @@ export const escHtml = (string) =>
70
77
  // @ts-expect-error
71
78
  string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string))
72
79
 
80
+ const OBJECT = 'object'
81
+ const FUNCTION = 'function'
82
+
73
83
  /**
74
84
  * escape any value for HTML context; objects and functions are stored in the render cache
75
85
  * @param {any} any
@@ -80,7 +90,7 @@ const escValue = (any) => {
80
90
  // @ts-expect-error
81
91
  return any
82
92
  }
83
- if (['object', 'function'].includes(typeof any)) {
93
+ if ([OBJECT, FUNCTION].includes(typeof any)) {
84
94
  const key = globalRenderCache.set(any)
85
95
  return unsafeHtml(key)
86
96
  }
@@ -129,6 +139,9 @@ export function render(node, template, handlers = {}) {
129
139
  return refs
130
140
  }
131
141
 
142
+ const REF = 'ref'
143
+ const REF_Q = '[ref]'
144
+
132
145
  /**
133
146
  * Post-processing of rendered nodes to handle special attributes:
134
147
  *
@@ -149,6 +162,7 @@ export function render(node, template, handlers = {}) {
149
162
  */
150
163
  export function renderAttrs(node, handlers = {}, refs = {}) {
151
164
  if (node.nodeType === Node.ELEMENT_NODE) {
165
+ const rmFns = []
152
166
  for (let attr of node.attributes) {
153
167
  const startsWith = attr.name[0]
154
168
  const name = attr.name.slice(1)
@@ -164,7 +178,7 @@ export function renderAttrs(node, handlers = {}, refs = {}) {
164
178
  } else if (attr.name === '...') {
165
179
  // spread attribute
166
180
  const obj = globalRenderCache.get(attr.value)
167
- if (obj && typeof obj === 'object') {
181
+ if (obj && typeof obj === OBJECT) {
168
182
  for (const [k, v] of Object.entries(obj)) {
169
183
  node[k] = v
170
184
  }
@@ -180,25 +194,36 @@ export function renderAttrs(node, handlers = {}, refs = {}) {
180
194
  const fn = globalRenderCache.get(handlerName)
181
195
  if (fn) {
182
196
  node.addEventListener(name, (e) => fn(e))
183
- } else if (typeof handlers[handlerName] === 'function') {
197
+ } else if (typeof handlers[handlerName] === FUNCTION) {
184
198
  node.addEventListener(name, (e) => handlers[handlerName](e))
185
199
  }
186
200
  rm = 1
187
- } else if (attr.name === 'ref') {
201
+ } else if (attr.name === REF) {
188
202
  // element reference - remove as well to prevent collection by other processors
189
203
  const refName = attr.value
190
204
  refs[refName] = node
191
205
  rm = 1
192
206
  }
193
207
  if (rm) {
194
- requestAnimationFrame(() => {
195
- node.removeAttribute(attr.name)
196
- })
208
+ rmFns.push([node, attr.name])
197
209
  }
198
210
  }
211
+ // @ts-expect-error
212
+ rmFns.forEach(([node, name]) => node.removeAttribute(name))
213
+ }
214
+ // early abort if custom element but resolve slotted refs
215
+ if (customElements.get(node.localName)) {
216
+ const q = node.querySelectorAll(REF_Q)
217
+ for (let el of q) {
218
+ const refName = el.getAttribute(REF)
219
+ if (refName && !refs[refName]) {
220
+ refs[refName] = el
221
+ }
222
+ }
223
+ return refs
199
224
  }
200
- // early abort if no children or custom element
201
- if (!node.children?.length || customElements.get(node.localName)) {
225
+ // early abort if no children
226
+ if (!node.children?.length) {
202
227
  return refs
203
228
  }
204
229
  for (let child of Array.from(node.children)) {
package/src/index.js CHANGED
@@ -10,11 +10,18 @@ export {
10
10
  * @typedef {import('./element.js').HostController} HostController
11
11
  */
12
12
  export { MiElement, convertType, define } from './element.js'
13
- export { unsafeHtml, html, escHtml, render, renderAttrs } from './html.js'
13
+ export { html, unsafeHtml, escHtml, render, renderAttrs } from './html.js'
14
14
  export { refsBySelector } from './refs.js'
15
15
  /**
16
16
  * @typedef {import('./store.js').Action} Action
17
17
  */
18
18
  export { Store } from './store.js'
19
- export { classNames, styleMap, addGlobalStyles, css } from './styling.js'
19
+ export {
20
+ classNames,
21
+ styleMap,
22
+ addGlobalStyles,
23
+ css,
24
+ unsafeCss,
25
+ escCss
26
+ } from './styling.js'
20
27
  export { default as Signal } from 'mi-signal'
package/src/styling.js CHANGED
@@ -11,8 +11,6 @@ export const classNames = (...args) => {
11
11
  if (!arg) return
12
12
  if (typeof arg === 'string') {
13
13
  classList.push(arg)
14
- } else if (Array.isArray(arg)) {
15
- classList.push(classNames(...arg))
16
14
  } else if (typeof arg === 'object') {
17
15
  Object.entries(arg).forEach(([key, value]) => {
18
16
  if (value) {
@@ -77,9 +75,41 @@ export function addGlobalStyles(renderRoot) {
77
75
  renderRoot.adoptedStyleSheets.push(...getGlobalStyleSheets())
78
76
  }
79
77
 
78
+ /**
79
+ * A helper class to avoid double escaping of HTML strings
80
+ */
81
+ class UnsafeCss extends String {}
82
+
83
+ /**
84
+ * tag a string as css for not to be escaped
85
+ * @param {string} str
86
+ * @returns {string}
87
+ */
88
+ // @ts-expect-error
89
+ export const unsafeCss = (str) => new UnsafeCss(str)
90
+
91
+ const escMap = {
92
+ '&': '\\26 ',
93
+ '<': '\\3c ',
94
+ '>': '\\3e '
95
+ }
96
+
97
+ const esc = (string) => string.replace(/[&<>]/g, (tag) => escMap[tag])
98
+
99
+ /**
100
+ * @see https://mathiasbynens.be/notes/css-escapes
101
+ * Escape a value interpolated into a css tagged template literal.
102
+ * Prevents injection of closing style tags or unexpected CSS constructs.
103
+ * @param {*} string
104
+ * @returns {string}
105
+ */
106
+ export const escCss = (string) =>
107
+ // @ts-expect-error
108
+ string instanceof UnsafeCss ? string : unsafeCss(esc('' + string))
109
+
80
110
  /**
81
111
  * Helper literal to show css styles in JS e.g. with
82
112
  * https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
83
113
  */
84
114
  export const css = (strings, ...values) =>
85
- String.raw({ raw: strings }, ...values)
115
+ String.raw({ raw: strings }, ...values.map(escCss))
package/types/html.d.ts CHANGED
@@ -40,8 +40,9 @@ declare class UnsafeHtml extends String {
40
40
  */
41
41
  declare class RenderCache {
42
42
  cnt: number;
43
- map: Map<any, any>;
44
- cache: WeakMap<WeakKey, any>;
43
+ cache: Map<any, any>;
44
+ last: number;
45
+ get size(): number;
45
46
  _inc(): number;
46
47
  clear(): void;
47
48
  set(value: any): string;
package/types/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export type HostController = import("./element.js").HostController;
6
6
  export type Action = import("./store.js").Action;
7
7
  export { ContextConsumer, ContextProvider, ContextRequestEvent } from "./context.js";
8
8
  export { MiElement, convertType, define } from "./element.js";
9
- export { unsafeHtml, html, escHtml, render, renderAttrs } from "./html.js";
10
- export { classNames, styleMap, addGlobalStyles, css } from "./styling.js";
9
+ export { html, unsafeHtml, escHtml, render, renderAttrs } from "./html.js";
10
+ export { classNames, styleMap, addGlobalStyles, css, unsafeCss, escCss } from "./styling.js";
@@ -15,4 +15,6 @@ export function styleMap(map: {
15
15
  }, options?: {
16
16
  unit?: string | undefined;
17
17
  }): string;
18
+ export function unsafeCss(str: string): string;
19
+ export function escCss(string: any): string;
18
20
  export function css(strings: any, ...values: any[]): string;