mi-element 0.6.7 → 0.7.0
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/README.md +11 -9
- package/dist/context.js +6 -0
- package/dist/element.js +92 -109
- package/dist/escape.js +12 -5
- package/dist/index.js +5 -5
- package/dist/refs.js +1 -9
- package/dist/styling.js +12 -5
- package/docs/context.md +43 -27
- package/docs/controller.md +4 -0
- package/docs/element.md +47 -60
- package/docs/reactivity.md +2 -2
- package/docs/signal.md +13 -6
- package/docs/store.md +1 -0
- package/docs/styling.md +17 -5
- package/package.json +16 -20
- package/src/context.js +8 -0
- package/src/element.js +203 -197
- package/src/escape.js +17 -13
- package/src/index.js +4 -14
- package/src/refs.js +0 -24
- package/src/styling.js +19 -9
- package/types/context.d.ts +2 -0
- package/types/element.d.ts +53 -34
- package/types/escape.d.ts +1 -1
- package/types/index.d.ts +4 -8
- package/types/refs.d.ts +0 -11
- package/types/styling.d.ts +1 -3
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -1
- package/src/min.js +0 -17
- package/types/min.d.ts +0 -1
package/src/element.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { camelToKebabCase } from './case.js'
|
|
1
|
+
import { kebabToCamelCase, camelToKebabCase } from './case.js'
|
|
2
|
+
import { addGlobalStyles } from './styling.js'
|
|
3
|
+
import { refsBySelector } from './refs.js'
|
|
2
4
|
import { createSignal } from 'mi-signal'
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Mapping of attribute names to property names
|
|
8
|
+
*/
|
|
9
|
+
const nameMap = {
|
|
10
|
+
class: 'className',
|
|
11
|
+
for: 'htmlFor'
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* @typedef {object} HostController controller
|
|
6
16
|
* @property {() => void} hostConnected is called when host element is added to
|
|
@@ -16,12 +26,14 @@ import { createSignal } from 'mi-signal'
|
|
|
16
26
|
* @example
|
|
17
27
|
* ```js
|
|
18
28
|
* class Example extends MiElement {
|
|
19
|
-
* // define all observed attributes
|
|
20
|
-
* //
|
|
21
|
-
* //
|
|
22
|
-
* //
|
|
29
|
+
* // define all observed attributes and define its type,
|
|
30
|
+
* // either use String/'', Number/0, Boolean/true, Array/[], Object/{}.
|
|
31
|
+
* // Objects and Arrays are deserialized from JSON.
|
|
32
|
+
* // Attributes are accessible via `this[prop]` as camelCased properties.
|
|
33
|
+
* // camelCased attributes are converted to kebab-case automatically.
|
|
34
|
+
* // Avoid using attributes which are HTMLElement properties e.g. className
|
|
23
35
|
* static get attributes () {
|
|
24
|
-
* return { text: '
|
|
36
|
+
* return { text: '', num: Number }
|
|
25
37
|
* }
|
|
26
38
|
* render() {
|
|
27
39
|
* this.renderRoot.innerHTML = `<div></div>`
|
|
@@ -44,24 +56,14 @@ import { createSignal } from 'mi-signal'
|
|
|
44
56
|
* ```
|
|
45
57
|
*/
|
|
46
58
|
export class MiElement extends HTMLElement {
|
|
47
|
-
/** all
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* @type {Map<string, string>}
|
|
53
|
-
*/
|
|
54
|
-
#attrLc = new Map()
|
|
55
|
-
/**
|
|
56
|
-
* initial types (from `static get attributes() { return {} }`)
|
|
57
|
-
* Map<camelCased attribute name, type as string>
|
|
58
|
-
* @type {Map<string,string>}
|
|
59
|
-
*/
|
|
60
|
-
#types = new Map()
|
|
59
|
+
/** all properties are signals! */
|
|
60
|
+
_props = {}
|
|
61
|
+
/** changed properties */
|
|
62
|
+
#changedProps = {}
|
|
63
|
+
|
|
61
64
|
#disposers = new Set()
|
|
62
65
|
#controllers = new Set()
|
|
63
|
-
#
|
|
64
|
-
#dedupe = false
|
|
66
|
+
#updateRequested = false
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Default options used when calling `attachShadow`. Used in
|
|
@@ -69,7 +71,9 @@ export class MiElement extends HTMLElement {
|
|
|
69
71
|
* If override is `null`, no shadow-root will be attached.
|
|
70
72
|
* @type {{mode: string}|null}
|
|
71
73
|
*/
|
|
72
|
-
static
|
|
74
|
+
static get shadowRootInit() {
|
|
75
|
+
return { mode: 'open' }
|
|
76
|
+
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
79
|
* defines template for render().
|
|
@@ -78,83 +82,75 @@ export class MiElement extends HTMLElement {
|
|
|
78
82
|
static template
|
|
79
83
|
|
|
80
84
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @returns {Record<
|
|
83
|
-
*/
|
|
84
|
-
static get attributes() {
|
|
85
|
-
return {}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* observable properties
|
|
89
|
-
* @returns {Record<PropertyKey, unknown>|{}}
|
|
85
|
+
* used to define observedAttributes and booleanAttributes during registration
|
|
86
|
+
* @returns {Record<string, {attribute?: boolean, type?:String|Number|Boolean|Array|Object, initial?: any}>} attribute name to isBoolean map
|
|
90
87
|
*/
|
|
91
88
|
static get properties() {
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
constructor() {
|
|
96
|
-
super()
|
|
97
|
-
// @ts-expect-error
|
|
98
|
-
this.#observedAttributes(this.constructor.attributes)
|
|
89
|
+
// to be overridden
|
|
99
90
|
// @ts-expect-error
|
|
100
|
-
|
|
91
|
+
return undefined
|
|
101
92
|
}
|
|
102
|
-
|
|
103
|
-
#observe(name, initialValue) {
|
|
104
|
-
this.#attr[name] = createSignal(initialValue)
|
|
105
|
-
Object.defineProperty(this, name, {
|
|
106
|
-
enumerable: true,
|
|
107
|
-
get() {
|
|
108
|
-
return this.#attr[name].get()
|
|
109
|
-
},
|
|
110
|
-
set(newValue) {
|
|
111
|
-
const oldValue = this.#attr[name].get()
|
|
112
|
-
if (oldValue === newValue) return
|
|
113
|
-
this.#attr[name].set(newValue)
|
|
114
|
-
this.#changedAttr[name] = oldValue
|
|
115
|
-
this.requestUpdate()
|
|
116
|
-
}
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
|
|
120
93
|
/**
|
|
121
|
-
*
|
|
122
|
-
* @param {Record<string, any>} [attributes]
|
|
94
|
+
* @returns {string[]}
|
|
123
95
|
*/
|
|
124
|
-
|
|
125
|
-
for (const [name, value] of Object.entries(attributes)) {
|
|
126
|
-
const initial = initialValueType(value)
|
|
127
|
-
this.#types.set(name, initial.type)
|
|
128
|
-
this.#attrLc.set(name.toLowerCase(), name)
|
|
129
|
-
this.#attrLc.set(camelToKebabCase(name), name)
|
|
130
|
-
this.#observe(name, initial.value)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
96
|
+
static observedAttributes = []
|
|
133
97
|
|
|
134
98
|
/**
|
|
135
|
-
*
|
|
136
|
-
* @param {Record<string, any>} [properties]
|
|
99
|
+
* @returns {string} css styles
|
|
137
100
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
101
|
+
static styles = ''
|
|
102
|
+
/**
|
|
103
|
+
* Whether to use global styles instead of scoped styles.
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
static get useGlobalStyles() {
|
|
107
|
+
return false
|
|
145
108
|
}
|
|
146
109
|
|
|
147
110
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* @returns
|
|
111
|
+
* Define createSignal function for properties.
|
|
112
|
+
* Signal values are set with the .value property
|
|
113
|
+
* @returns {import('mi-signal').createSignal|null} createSignal function
|
|
151
114
|
*/
|
|
152
|
-
|
|
153
|
-
return this.#attrLc.get(name) || name
|
|
154
|
-
}
|
|
115
|
+
static createSignal = createSignal
|
|
155
116
|
|
|
156
|
-
|
|
157
|
-
|
|
117
|
+
constructor() {
|
|
118
|
+
super()
|
|
119
|
+
// @ts-expect-error
|
|
120
|
+
const { createSignal, properties } = this.constructor
|
|
121
|
+
for (const [name, { initial }] of Object.entries(properties)) {
|
|
122
|
+
// allow overwrites with setter, getters
|
|
123
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
124
|
+
this.constructor.prototype,
|
|
125
|
+
name
|
|
126
|
+
)
|
|
127
|
+
if (createSignal) {
|
|
128
|
+
this._props[name] = createSignal()
|
|
129
|
+
}
|
|
130
|
+
Object.defineProperty(this, name, {
|
|
131
|
+
get() {
|
|
132
|
+
if (descriptor?.get) {
|
|
133
|
+
return descriptor.get.call(this)
|
|
134
|
+
}
|
|
135
|
+
return createSignal ? this._props[name].value : this._props[name]
|
|
136
|
+
},
|
|
137
|
+
set(value) {
|
|
138
|
+
const oldValue = this[name]
|
|
139
|
+
if (descriptor?.set) {
|
|
140
|
+
descriptor.set.call(this, value)
|
|
141
|
+
} else if (createSignal) {
|
|
142
|
+
this._props[name].value = value
|
|
143
|
+
} else {
|
|
144
|
+
this._props[name] = value
|
|
145
|
+
}
|
|
146
|
+
if (oldValue !== this[name]) {
|
|
147
|
+
this.requestUpdate({ [name]: value })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
// set initial value
|
|
152
|
+
this[name] = initial
|
|
153
|
+
}
|
|
158
154
|
}
|
|
159
155
|
|
|
160
156
|
/**
|
|
@@ -163,17 +159,17 @@ export class MiElement extends HTMLElement {
|
|
|
163
159
|
*/
|
|
164
160
|
connectedCallback() {
|
|
165
161
|
this.#controllers.forEach((controller) => controller.hostConnected?.())
|
|
166
|
-
// create render root
|
|
167
162
|
// @ts-expect-error
|
|
168
|
-
const {
|
|
169
|
-
this.renderRoot =
|
|
170
|
-
? (this.shadowRoot ?? this.attachShadow(
|
|
163
|
+
const { shadowRootInit, useGlobalStyles, template } = this.constructor
|
|
164
|
+
this.renderRoot = shadowRootInit
|
|
165
|
+
? (this.shadowRoot ?? this.attachShadow(shadowRootInit))
|
|
171
166
|
: this
|
|
172
167
|
this.addTemplate(template)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.
|
|
168
|
+
if (useGlobalStyles) {
|
|
169
|
+
addGlobalStyles(this.renderRoot)
|
|
170
|
+
}
|
|
171
|
+
this.render() // initial render
|
|
172
|
+
this.requestUpdate() // request initial update
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
/**
|
|
@@ -188,75 +184,39 @@ export class MiElement extends HTMLElement {
|
|
|
188
184
|
|
|
189
185
|
/**
|
|
190
186
|
* @param {string} name change attribute
|
|
191
|
-
* @param {any}
|
|
187
|
+
* @param {any} _oldValue
|
|
192
188
|
* @param {any} newValue new value
|
|
193
189
|
*/
|
|
194
|
-
attributeChangedCallback(name,
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Set string and number attributes on element only. Set all other values as
|
|
209
|
-
* properties to avoid type conversion to and from string
|
|
210
|
-
* @param {string} name
|
|
211
|
-
* @param {any} newValue
|
|
212
|
-
*/
|
|
213
|
-
setAttribute(name, newValue) {
|
|
214
|
-
const attr = this.#getName(name)
|
|
215
|
-
// only allow to change observedAttributes
|
|
216
|
-
if (!(attr in this.#attr)) {
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
const type = this.#getType(attr)
|
|
220
|
-
|
|
221
|
-
// only set string values in these cases
|
|
222
|
-
if (type === 'Boolean') {
|
|
223
|
-
if (newValue === true || newValue === '') {
|
|
224
|
-
super.setAttribute(name, '')
|
|
225
|
-
} else {
|
|
226
|
-
super.removeAttribute(name)
|
|
190
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
191
|
+
const camelName = nameMap[name] ?? kebabToCamelCase(name)
|
|
192
|
+
// @ts-expect-error
|
|
193
|
+
const properties = this.constructor?.properties
|
|
194
|
+
const { type } = properties?.[camelName] ?? {}
|
|
195
|
+
const coercedValue = convertType(newValue, type)
|
|
196
|
+
// set data-* attributes to dataset
|
|
197
|
+
if (name.startsWith('data-')) {
|
|
198
|
+
const datasetName = kebabToCamelCase(name.substring(5))
|
|
199
|
+
// datasetName may be empty if attribute is just 'data-'
|
|
200
|
+
if (datasetName) {
|
|
201
|
+
this.dataset[datasetName] = coercedValue
|
|
227
202
|
}
|
|
228
|
-
} else if (['String', 'Number'].includes(type ?? '') || newValue === true) {
|
|
229
|
-
super.setAttribute(name, newValue)
|
|
230
|
-
} else {
|
|
231
|
-
this.#changedAttr[attr] = this[attr]
|
|
232
|
-
this[attr] = newValue
|
|
233
|
-
this.requestUpdate()
|
|
234
203
|
}
|
|
204
|
+
this[camelName] = coercedValue
|
|
235
205
|
}
|
|
236
206
|
|
|
237
207
|
/**
|
|
238
|
-
*
|
|
239
|
-
* @param {Record<string,any>} [_changedAttributes] previous values of changed attributes
|
|
240
|
-
* @returns {boolean}
|
|
208
|
+
* @param {Record<string, any>} [changedProps]
|
|
241
209
|
*/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
requestAnimationFrame(() => {
|
|
253
|
-
this.#dedupe = false
|
|
254
|
-
// reset changed attributes
|
|
255
|
-
const _changedAttributes = this.#changedAttr
|
|
256
|
-
this.#changedAttr = {}
|
|
257
|
-
if (this.shouldUpdate(_changedAttributes)) {
|
|
258
|
-
this.update(_changedAttributes)
|
|
259
|
-
}
|
|
210
|
+
requestUpdate(changedProps) {
|
|
211
|
+
this.#changedProps = { ...this.#changedProps, ...changedProps }
|
|
212
|
+
if (this.#updateRequested || !this.renderRoot) return
|
|
213
|
+
this.#updateRequested = true
|
|
214
|
+
window.requestAnimationFrame(() => {
|
|
215
|
+
this.#updateRequested = false
|
|
216
|
+
// reset changed properties
|
|
217
|
+
const changedProps = this.#changedProps
|
|
218
|
+
this.#changedProps = {}
|
|
219
|
+
this.update(changedProps)
|
|
260
220
|
})
|
|
261
221
|
}
|
|
262
222
|
|
|
@@ -275,14 +235,18 @@ export class MiElement extends HTMLElement {
|
|
|
275
235
|
/**
|
|
276
236
|
* initial rendering
|
|
277
237
|
*/
|
|
278
|
-
render() {
|
|
238
|
+
render() {
|
|
239
|
+
// to be overridden
|
|
240
|
+
}
|
|
279
241
|
|
|
280
242
|
/**
|
|
281
243
|
* called every time the components needs a render update
|
|
282
|
-
* @param {Record<string,any>} [
|
|
283
|
-
* attributes
|
|
244
|
+
* @param {Record<string, any>} [_changedProps] previous values of changed
|
|
245
|
+
* properties (attributes)
|
|
284
246
|
*/
|
|
285
|
-
update(
|
|
247
|
+
update(_changedProps) {
|
|
248
|
+
// to be overridden
|
|
249
|
+
}
|
|
286
250
|
|
|
287
251
|
/**
|
|
288
252
|
* Adds listener function for eventName. listener is removed before component
|
|
@@ -341,29 +305,68 @@ export class MiElement extends HTMLElement {
|
|
|
341
305
|
removeController(controller) {
|
|
342
306
|
this.#controllers.delete(controller)
|
|
343
307
|
}
|
|
308
|
+
|
|
309
|
+
refsBySelector(selectors) {
|
|
310
|
+
return refsBySelector(this.renderRoot, selectors)
|
|
311
|
+
}
|
|
344
312
|
}
|
|
345
313
|
|
|
346
314
|
/**
|
|
347
315
|
* defines a custom element adding observedAttributes from default static
|
|
348
316
|
* attributes
|
|
349
|
-
* NOTE: camelCased attributes get lowercased!
|
|
317
|
+
* NOTE: camelCased attributes on DOM elements get lowercased by the browser!
|
|
318
|
+
* Prefer using static get attributes() where camelCased names are converted to
|
|
319
|
+
* kebab-case automatically.
|
|
350
320
|
* ```html
|
|
351
321
|
* <custom-element myAttr="1">
|
|
352
322
|
* <!-- is equal to -->
|
|
353
323
|
* <custom-element myattr="1">
|
|
354
324
|
* ```
|
|
355
|
-
* @param {string}
|
|
356
|
-
* @param {typeof MiElement}
|
|
357
|
-
* @param {
|
|
325
|
+
* @param {string} tagName custom element tag
|
|
326
|
+
* @param {typeof MiElement} elementClass
|
|
327
|
+
* @param {{usedCssPrefix?: string, cssPrefix?: string, styles?: string}} [options]
|
|
358
328
|
*/
|
|
359
|
-
export const define = (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
329
|
+
export const define = (tagName, elementClass, options) => {
|
|
330
|
+
if (customElements.get(tagName)) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
const { usedCssPrefix = '', cssPrefix = '', styles } = options || {}
|
|
334
|
+
if (elementClass.properties) {
|
|
335
|
+
// only lowercase attribute names are observed!
|
|
336
|
+
const observedAttrs = []
|
|
337
|
+
for (const [name, { attribute = true }] of Object.entries(
|
|
338
|
+
elementClass.properties
|
|
339
|
+
)) {
|
|
340
|
+
if (attribute) {
|
|
341
|
+
observedAttrs.push(camelToKebabCase(name))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
Object.defineProperty(elementClass, 'observedAttributes', {
|
|
345
|
+
get() {
|
|
346
|
+
return observedAttrs
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
} else if (elementClass.observedAttributes) {
|
|
350
|
+
const properties = elementClass.observedAttributes.reduce((acc, attr) => {
|
|
351
|
+
const camelName = kebabToCamelCase(attr)
|
|
352
|
+
acc[camelName] = {}
|
|
353
|
+
return acc
|
|
354
|
+
}, {})
|
|
355
|
+
Object.defineProperty(elementClass, 'properties', {
|
|
356
|
+
get() {
|
|
357
|
+
return properties
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
if (elementClass.styles) {
|
|
362
|
+
elementClass.styles =
|
|
363
|
+
styles ||
|
|
364
|
+
(usedCssPrefix === cssPrefix
|
|
365
|
+
? elementClass.styles
|
|
366
|
+
: elementClass.styles.replaceAll(`--${usedCssPrefix}-`, cssPrefix))
|
|
367
|
+
}
|
|
368
|
+
renderTemplate(elementClass)
|
|
369
|
+
window.customElements.define(tagName, elementClass)
|
|
367
370
|
}
|
|
368
371
|
|
|
369
372
|
// --- utils
|
|
@@ -381,35 +384,38 @@ const renderTemplate = (element) => {
|
|
|
381
384
|
element.template = el
|
|
382
385
|
}
|
|
383
386
|
|
|
384
|
-
const initialValueType = (value) => {
|
|
385
|
-
switch (value) {
|
|
386
|
-
case Boolean:
|
|
387
|
-
return { value: undefined, type: 'Boolean' }
|
|
388
|
-
case Number:
|
|
389
|
-
return { value: undefined, type: 'Number' }
|
|
390
|
-
case String:
|
|
391
|
-
return { value: undefined, type: 'String' }
|
|
392
|
-
default:
|
|
393
|
-
return { value, type: toString.call(value).slice(8, -1) }
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
387
|
const toNumber = (any) => {
|
|
398
388
|
const n = Number(any)
|
|
399
|
-
return isNaN(n) ?
|
|
389
|
+
return isNaN(n) ? 0 : n
|
|
400
390
|
}
|
|
401
391
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
392
|
+
const toJson = (any) => {
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(any)
|
|
395
|
+
} catch {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* convert a attribute string value to typed value
|
|
402
|
+
* @param {string} value
|
|
403
|
+
* @param {typeof Boolean|typeof Number|typeof String|typeof Array|typeof Object} type
|
|
404
|
+
* @returns {any}
|
|
405
|
+
*/
|
|
406
|
+
export const convertType = (value, type) => {
|
|
407
|
+
if (type === Boolean) {
|
|
408
|
+
// false: removeAttribute -> null, true: setAttribute -> ''
|
|
409
|
+
return value !== null
|
|
410
|
+
}
|
|
411
|
+
if (type === Number) {
|
|
412
|
+
return toNumber(value)
|
|
413
|
+
}
|
|
414
|
+
if (type === Array) {
|
|
415
|
+
return toJson(value) ?? value.split(',').map((v) => v.trim())
|
|
416
|
+
}
|
|
417
|
+
if (type === Object) {
|
|
418
|
+
return toJson(value)
|
|
413
419
|
}
|
|
414
|
-
return
|
|
420
|
+
return value
|
|
415
421
|
}
|
package/src/escape.js
CHANGED
|
@@ -11,9 +11,20 @@ export const unsafeHtml = (str) => new UnsafeHtml(str)
|
|
|
11
11
|
const escMap = {
|
|
12
12
|
'&': '&',
|
|
13
13
|
'<': '<',
|
|
14
|
-
'>': '>'
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
'>': '>'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let esc = (string) => {
|
|
18
|
+
return string.replace(/&/g, '&').replace(/[&<>]/g, (tag) => escMap[tag])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof document !== 'undefined') {
|
|
22
|
+
// in browser environment, use DOM to escape
|
|
23
|
+
esc = (string) => {
|
|
24
|
+
const div = document.createElement('div')
|
|
25
|
+
div.textContent = string
|
|
26
|
+
return div.innerHTML
|
|
27
|
+
}
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -26,13 +37,7 @@ const escMap = {
|
|
|
26
37
|
*/
|
|
27
38
|
export const escHtml = (string) =>
|
|
28
39
|
// @ts-expect-error
|
|
29
|
-
string instanceof UnsafeHtml
|
|
30
|
-
? string
|
|
31
|
-
: unsafeHtml(
|
|
32
|
-
('' + string)
|
|
33
|
-
.replace(/&/g, '&')
|
|
34
|
-
.replace(/[&<>'"]/g, (tag) => escMap[tag])
|
|
35
|
-
)
|
|
40
|
+
string instanceof UnsafeHtml ? string : unsafeHtml(esc('' + string))
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* template literal to HTML escape all values preventing XSS;
|
|
@@ -41,11 +46,10 @@ export const escHtml = (string) =>
|
|
|
41
46
|
* @param {...any} values
|
|
42
47
|
* @returns {string}
|
|
43
48
|
* @example
|
|
44
|
-
* const
|
|
45
|
-
* const list = esc`<ul>${data.map(item => esc`<li>${item}</li>`)}</ul>`
|
|
49
|
+
* const list = html`<ul>${['<foo', 'bar>'].map(item => html`<li>${item}</li>`)}</ul>`
|
|
46
50
|
* // '<ul><li><foo</li><li>bar></li></ul>'
|
|
47
51
|
*/
|
|
48
|
-
export const
|
|
52
|
+
export const html = (strings, ...values) =>
|
|
49
53
|
unsafeHtml(
|
|
50
54
|
String.raw(
|
|
51
55
|
{ raw: strings },
|
package/src/index.js
CHANGED
|
@@ -10,21 +10,11 @@ export {
|
|
|
10
10
|
* @typedef {import('./element.js').HostController} HostController
|
|
11
11
|
*/
|
|
12
12
|
export { MiElement, convertType, define } from './element.js'
|
|
13
|
-
export { unsafeHtml,
|
|
14
|
-
export {
|
|
15
|
-
/**
|
|
16
|
-
* @template T
|
|
17
|
-
* @typedef {import('mi-signal').SignalOptions<T>} SignalOptions<T>
|
|
18
|
-
*/
|
|
19
|
-
export {
|
|
20
|
-
default as Signal,
|
|
21
|
-
State,
|
|
22
|
-
createSignal,
|
|
23
|
-
effect,
|
|
24
|
-
Computed
|
|
25
|
-
} from 'mi-signal'
|
|
13
|
+
export { unsafeHtml, html, escHtml } from './escape.js'
|
|
14
|
+
export { refsBySelector } from './refs.js'
|
|
26
15
|
/**
|
|
27
16
|
* @typedef {import('./store.js').Action} Action
|
|
28
17
|
*/
|
|
29
18
|
export { Store } from './store.js'
|
|
30
|
-
export {
|
|
19
|
+
export { classNames, styleMap, addGlobalStyles, css } from './styling.js'
|
|
20
|
+
export { default as Signal } from 'mi-signal'
|
package/src/refs.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
import { kebabToCamelCase } from './case.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Helper function to find `id` attributes in `container`s node tree.
|
|
5
|
-
* id names are camelCased, e.g. 'list-container' becomes 'listContainer'
|
|
6
|
-
* @param {Element} container root element
|
|
7
|
-
* @returns {Record<string, Node>|{}} record of found references
|
|
8
|
-
* @example
|
|
9
|
-
* el.innerHTML = `<p id>unnamed <span id="named">and named</span> reference</p>`
|
|
10
|
-
* references = refs(el)
|
|
11
|
-
* //> references = { p: <p>, named: <span> }
|
|
12
|
-
*/
|
|
13
|
-
export function refsById(container) {
|
|
14
|
-
const nodes = container.querySelectorAll?.('[id]') || []
|
|
15
|
-
const found = {}
|
|
16
|
-
for (const node of nodes) {
|
|
17
|
-
const name = kebabToCamelCase(
|
|
18
|
-
node.getAttribute('id') || node.nodeName.toLowerCase()
|
|
19
|
-
)
|
|
20
|
-
found[name] = node
|
|
21
|
-
}
|
|
22
|
-
return found
|
|
23
|
-
}
|
|
24
|
-
|
|
25
1
|
/**
|
|
26
2
|
* Helper function to gather references by a map of selectors
|
|
27
3
|
* @param {Element} container root element
|
package/src/styling.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { camelToKebabCase } from './case.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param
|
|
4
|
+
* conditionally joining classNames
|
|
5
|
+
* @param {...any} args
|
|
6
6
|
* @returns {string}
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
export const classNames = (...args) => {
|
|
9
|
+
const classList = []
|
|
10
|
+
args.forEach((arg) => {
|
|
11
|
+
if (!arg) return
|
|
12
|
+
if (typeof arg === 'string') {
|
|
13
|
+
classList.push(arg)
|
|
14
|
+
} else if (Array.isArray(arg)) {
|
|
15
|
+
classList.push(classNames(...arg))
|
|
16
|
+
} else if (typeof arg === 'object') {
|
|
17
|
+
Object.entries(arg).forEach(([key, value]) => {
|
|
18
|
+
if (value) {
|
|
19
|
+
classList.push(key)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
return classList.join(' ')
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
/**
|
package/types/context.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export class ContextProvider<T> implements HostController {
|
|
|
22
22
|
* @returns {T|null|undefined}
|
|
23
23
|
*/
|
|
24
24
|
get(): T | null | undefined;
|
|
25
|
+
set value(newValue: T | null | undefined);
|
|
26
|
+
get value(): T | null | undefined;
|
|
25
27
|
/**
|
|
26
28
|
* @private
|
|
27
29
|
* @param {ContextRequestEvent} ev
|