frontacles 0.3.0 → 0.5.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/CHANGELOG.md +36 -4
- package/README.md +184 -30
- package/package.json +16 -14
- package/src/dom/index.js +121 -0
- package/src/index.js +1 -0
- package/src/url/email.js +18 -1
- package/types/index.d.ts +20 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,45 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
6
6
|
|
|
7
7
|
Nothing for now.
|
|
8
8
|
|
|
9
|
+
<!-- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.5.0...main). -->
|
|
10
|
+
|
|
9
11
|
<!-- ⚠️ Before a new release, make sure the documentation doesn't contain any **unreleased** mention. -->
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
## v0.5.0 (2026-01-24)
|
|
14
|
+
|
|
15
|
+
Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.4.0...0.5.0).
|
|
16
|
+
|
|
17
|
+
### New
|
|
18
|
+
|
|
19
|
+
- Add [`setAttributes`](./README.md#setattributes), a function to update HTML attributes on any number of elements.
|
|
20
|
+
|
|
21
|
+
### Under the hood
|
|
22
|
+
|
|
23
|
+
- Setup tests for DOM utilities using Playwright via Vitest browser mode.
|
|
24
|
+
- Migrate types testing from `tsd` to TSTyche.
|
|
25
|
+
- Bump Node version from 20 to 22.
|
|
26
|
+
|
|
27
|
+
## v0.4.0 (2025-03-08)
|
|
28
|
+
|
|
29
|
+
Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.3.0...0.4.0).
|
|
30
|
+
|
|
31
|
+
### New
|
|
32
|
+
|
|
33
|
+
- Add [`isEmail`](./README.md#isemail) to validate emails.
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
|
|
37
|
+
- It is now more explicit in types that `Email.canParse` expects a `string` or a `Stringable` (changed from `any|Email` to `any|string|Email|Stringable`).
|
|
38
|
+
- Rephrase email documentation.
|
|
39
|
+
|
|
40
|
+
### Under the hood
|
|
41
|
+
|
|
42
|
+
- Centralize all benchmarks in [`./benchs`](./benchs).
|
|
43
|
+
- Benchmark [`Email`](./benchs/url).
|
|
12
44
|
|
|
13
45
|
## v0.3.0 (2025-03-06)
|
|
14
46
|
|
|
15
|
-
Compare with [
|
|
47
|
+
Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.2.2...0.3.0).
|
|
16
48
|
|
|
17
49
|
### New
|
|
18
50
|
|
|
@@ -32,7 +64,7 @@ Compare with [last published version](https://github.com/frontacles/frontacles/c
|
|
|
32
64
|
### Under the hood
|
|
33
65
|
|
|
34
66
|
- Shorten `round` by a couple of Bytes.
|
|
35
|
-
- Benchmark [`round` implementations](./
|
|
67
|
+
- Benchmark [`round` implementations](./benchs/math)
|
|
36
68
|
- Add [Valibot test suite](./src/url/test-utils/valibot-suite.js) to `Email` (all tests are passing!).
|
|
37
69
|
|
|
38
70
|
### Documentation
|
|
@@ -43,7 +75,7 @@ Compare with [last published version](https://github.com/frontacles/frontacles/c
|
|
|
43
75
|
|
|
44
76
|
## v0.2.2 (2025-03-01)
|
|
45
77
|
|
|
46
|
-
Compare with [
|
|
78
|
+
Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.2.1...0.2.2).
|
|
47
79
|
|
|
48
80
|
### Breaking
|
|
49
81
|
|
package/README.md
CHANGED
|
@@ -7,13 +7,131 @@ Cool utilities for front-end development (and potentially for Node).
|
|
|
7
7
|
|
|
8
8
|
We love tiny bits (using brotli compression):
|
|
9
9
|
|
|
10
|
-
|
|
|
11
|
-
| --- | --- | --- |
|
|
12
|
-
|
|
|
13
|
-
| math | [`
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
10
|
+
| category | util | size | description |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| DOM | [`setAttributes`](#setattributes) | 338 B | Updates attributes of HTML or SVG element(s). |
|
|
13
|
+
| math | [`clamp`](#clamp) | 35 B | Make sure a number stays in a given range. |
|
|
14
|
+
| math | [`round`](#round) | 38 B | Round a number to a given precision. |
|
|
15
|
+
| string | [`capitalize`](#capitalize) | 40 B | Capitalize the first letter of a string. |
|
|
16
|
+
| URL | [`isEmail`](#isemail) | 86 B | Wheither a variable is a valid email address. |
|
|
17
|
+
| URL | [`Email`](#email) | 173 B | An `Email` object with validation and separate access to an email username and domain. |
|
|
18
|
+
| | **everything** | 645 B | |
|
|
19
|
+
|
|
20
|
+
## DOM utils
|
|
21
|
+
|
|
22
|
+
### `setAttributes`
|
|
23
|
+
|
|
24
|
+
Updates attributes of HTML element(s).
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { setAttributes } from 'frontacles/dom'
|
|
28
|
+
|
|
29
|
+
const widget = getElementById('animal-widget')
|
|
30
|
+
|
|
31
|
+
// `<div name="Animal widget" loading="true">`
|
|
32
|
+
setAttributes(widget, {
|
|
33
|
+
name: 'Animal widget'
|
|
34
|
+
loading: true,
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To remove an attribute, set its value to `false`, `null` or `undefined`.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// `<div name="Animal widget">`
|
|
42
|
+
setAttributes(widget, { loading: false })
|
|
43
|
+
|
|
44
|
+
// `<div name="Animal widget" loading="false">`
|
|
45
|
+
setAttributes(widget, { loading: 'false' })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`setAttributes` also accepts multiple elements (array or [HTML collection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection)):
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const animals = widget.getElementsbyClassName('.list-item')
|
|
52
|
+
|
|
53
|
+
// `<li class="list-item" data-cat="animals">cat</li>`
|
|
54
|
+
setAttributes(animals, { 'data-cat': 'animals' })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When an attribute is an object, its properties are converted to `attrName-propName` attributes, making it helpful for any bulk update of `aria-*` or `data-*` attributes:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
setAttributes(el, {
|
|
61
|
+
loading: true,
|
|
62
|
+
aria: {
|
|
63
|
+
busy: true,
|
|
64
|
+
live: 'polite',
|
|
65
|
+
},
|
|
66
|
+
data: {
|
|
67
|
+
category: 'boats',
|
|
68
|
+
'max-items': 12,
|
|
69
|
+
},
|
|
70
|
+
user: {
|
|
71
|
+
id: 4,
|
|
72
|
+
name: 'Liz',
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The previous example gives:
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<div
|
|
81
|
+
loading="true"
|
|
82
|
+
aria-busy="true" aria-live="polite"
|
|
83
|
+
data-category="boats" data-max-items="12"
|
|
84
|
+
user-id="4" user-name="Liz"
|
|
85
|
+
>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Like for non-object attributes, using `false`, `null` or `undefined` as property value will remove the matching attribute from the HTML:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
setAttributes(el, { data: { 'max-items': null }}) // no more `data-max-items`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> [!NOTE]
|
|
95
|
+
> `data` is using [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) under the hood, but differs from it: `setAttributes` removes any attribute with a `null` value while `dataset` turn `null` into a string. In the previous example, `el.dataset.maxItems = null` would have given `data-max-items="null"` instead of removing the attribute.
|
|
96
|
+
|
|
97
|
+
`class` updates… CSS classes. It can be an array of CSS classes, a string containing one or more (space-separated) classes, or an object in the form of `{ className: state }`, defining which classes should be added or removed from the element.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// <div class="btn btn--xl">
|
|
101
|
+
setAttributes(el, { class: { btn: true, 'btn--xl': true }})
|
|
102
|
+
|
|
103
|
+
// <div class="btn btn--xl card__btn">
|
|
104
|
+
setAttributes(el, { class: ['card__btn'] })
|
|
105
|
+
|
|
106
|
+
// <div class="btn btn--xl card__btn card__btn--special">
|
|
107
|
+
setAttributes(el, { class: 'card__btn--special' })
|
|
108
|
+
|
|
109
|
+
// <div class="btn btn--xl card__btn card__btn--special card__btn--1 card__btn--2">
|
|
110
|
+
setAttributes(el, { class: 'card__btn--1 card__btn--2' })
|
|
111
|
+
|
|
112
|
+
// <div class="btn card__btn card__btn--special card__btn--1 card__btn--2">
|
|
113
|
+
setAttributes(el, { class: { 'btn--xl': false }})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
When `style` is an object, it behaves like [`HTMLElement.style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style). When it’s a string, it completely replaces the attribute.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
// <div style="color: red; opacity: 0.9">
|
|
120
|
+
setAttributes(el, {
|
|
121
|
+
style: {
|
|
122
|
+
color: 'red',
|
|
123
|
+
opacity: .9
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// <div style="color: red;">
|
|
128
|
+
setAttributes(el, { style: { opacity: null }})
|
|
129
|
+
|
|
130
|
+
// <div style="gap: 2px; opacity: .9;">
|
|
131
|
+
setAttributes(el, { style: 'gap: 2px; opacity: .9;')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`setAttributes` also works on any SVG or descendant elements.
|
|
17
135
|
|
|
18
136
|
## Math utils
|
|
19
137
|
|
|
@@ -46,10 +164,9 @@ clamp(Infinity, 0, 10) // 10
|
|
|
46
164
|
> [!NOTE]
|
|
47
165
|
> `clamp` mostly follows [`Math.clamp` TC39 proposal](https://github.com/tc39/proposal-math-clamp), except it doesn’t throw if you flip the order of the _min_ (2nd parameter) and _max_ (3rd parameter) numbers.
|
|
48
166
|
|
|
49
|
-
|
|
50
167
|
### `round`
|
|
51
168
|
|
|
52
|
-
Round a number to the (optionally) provided precision.
|
|
169
|
+
Round a number to the (optionally) provided decimal precision. The default precision is 0 (no decimal).
|
|
53
170
|
|
|
54
171
|
```js
|
|
55
172
|
import { round } from 'frontacles/math'
|
|
@@ -66,7 +183,7 @@ round(687.3456, -1) // 690
|
|
|
66
183
|
round(687.3456, -2) // 700
|
|
67
184
|
```
|
|
68
185
|
|
|
69
|
-
|
|
186
|
+
Using `Infinity` is also possible:
|
|
70
187
|
|
|
71
188
|
```js
|
|
72
189
|
round(Infinity, -2) // Infinity
|
|
@@ -97,11 +214,40 @@ capitalize('صحراء') // 'صحراء' (Arabic)
|
|
|
97
214
|
|
|
98
215
|
## URL utils
|
|
99
216
|
|
|
217
|
+
### `isEmail`
|
|
218
|
+
|
|
219
|
+
Tells whether a string is a valid email.
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
isEmail('someone@domain.tld') // true
|
|
223
|
+
isEmail('invalid@email.com:3000') // false
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
> [!TIP]
|
|
227
|
+
> Should I use `isEmail` or [`Email.canParse`](#emailcanparse) to validate emails?
|
|
228
|
+
>
|
|
229
|
+
> Short answer: use `isEmail`.
|
|
230
|
+
>
|
|
231
|
+
> <details>
|
|
232
|
+
> <summary>Nuanced answer</summary>
|
|
233
|
+
>
|
|
234
|
+
> Your use case:
|
|
235
|
+
>
|
|
236
|
+
> - If you **only need to validate** email addresses, use `isEmail`.
|
|
237
|
+
> - If you also need to be able to get or set an email username or hostname **independently**, use `Email.canParse`.
|
|
238
|
+
>
|
|
239
|
+
> When using the `Email` class, you can still use `isEmail` if you want ultra-performance (e.g. your Node API validates tons of emails per seconds) because `isEmail` is 6✕ faster, at the cost of a bit less than 100 Bytes (compressed).
|
|
240
|
+
>
|
|
241
|
+
> The reason `isEmail` is faster is that it relies on a single RegExp while `Email.canParse` uses the browser built-in, which results in a bit more of computation, but with less code. For now, it’s not planned to use `isEmail` implementation in `Email.canParse` as it would increase its size by 50 Bytes.
|
|
242
|
+
>
|
|
243
|
+
> Keep in mind that **`Email.canParse` is fast enough** for the 99% use cases. Despite their implementation difference, both behave the same and pass the same tests.
|
|
244
|
+
> </details>
|
|
245
|
+
|
|
100
246
|
### `Email`
|
|
101
247
|
|
|
102
|
-
A class to instantiate an `Email` object or validate email addresses.
|
|
248
|
+
A class to instantiate an `Email` object or validate email addresses. It extends the [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL) and has similar predictable behaviors.
|
|
103
249
|
|
|
104
|
-
|
|
250
|
+
#### `Email.constructor`
|
|
105
251
|
|
|
106
252
|
```js
|
|
107
253
|
import { Email } from 'frontacles/url/email'
|
|
@@ -109,7 +255,23 @@ import { Email } from 'frontacles/url/email'
|
|
|
109
255
|
const email = new Email('someone@domain.tld')
|
|
110
256
|
```
|
|
111
257
|
|
|
112
|
-
|
|
258
|
+
Trying to instantiate an Email with an invalid address will throw. This behaviour is similar to the [`URL` constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), since `Email` relies on it under the hood.
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
new Email('double@at@sign.com') // ❌ throw TypeError
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Another behaviour from `URL`: passing an `Email` object to the `Email` constructor or to [`Email.canParse`](#emailcanparse) is possible.
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
const email = new Email('someone@domain.tld')
|
|
268
|
+
const alsoEmail = new Email(email) // ✅ a new Email object!
|
|
269
|
+
Email.canParse(email) // ✅ true
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### `.username` and `.hostname`
|
|
273
|
+
|
|
274
|
+
Get or set the email username and hostname separately.
|
|
113
275
|
|
|
114
276
|
```js
|
|
115
277
|
email.username // 'someone'
|
|
@@ -121,35 +283,27 @@ email.hostname = 'newdomain.tld' // ✅ domain migrated
|
|
|
121
283
|
const { username, hostname } = new Email('someone@domain.tld')
|
|
122
284
|
```
|
|
123
285
|
|
|
124
|
-
|
|
286
|
+
#### `.toString`
|
|
287
|
+
|
|
288
|
+
In a string context, an `Email` object is automatically converted to a string, or manually by calling the `toString` method.
|
|
125
289
|
|
|
126
290
|
```js
|
|
127
291
|
console.log(`email: ${email}`) // 'email: someone@newdomain.tld'
|
|
128
292
|
console.log(email.toString()) // 'someone@newdomain.tld'
|
|
129
293
|
```
|
|
130
294
|
|
|
131
|
-
|
|
295
|
+
#### `Email.canParse`
|
|
132
296
|
|
|
133
|
-
|
|
134
|
-
Email.canParse('someone@domain.tld') // true
|
|
135
|
-
Email.canParse('invalid@email.com:3000') // false
|
|
136
|
-
```
|
|
297
|
+
Validate an email address with `Email.canParse`.
|
|
137
298
|
|
|
138
|
-
|
|
299
|
+
Unlike most libraries using [RegExp to validate a string is an email](https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/src/types.ts#L648-L663) (which is prone to [bugs](https://github.com/colinhacks/zod/issues/3913)), Frontacles `Email` relies on the built-in `URL` mechanisms, making it robust, and very likely RFC compliant. It passes [popular libraries test suites](./src/url/test-utils), and beyond.
|
|
139
300
|
|
|
140
301
|
```js
|
|
141
|
-
|
|
302
|
+
Email.canParse('someone@domain.tld') // true
|
|
303
|
+
Email.canParse('invalid@email.com:3000') // false
|
|
142
304
|
```
|
|
143
305
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```js
|
|
147
|
-
const email = new Email('someone@domain.tld')
|
|
148
|
-
|
|
149
|
-
const alsoEmail = new Email(email) // ✅ a new Email object!
|
|
150
|
-
|
|
151
|
-
Email.canParse(email) // ✅ true
|
|
152
|
-
```
|
|
306
|
+
If `canParse` is all you need from the `Email` class, consider using [isEmail](#isemail) instead.
|
|
153
307
|
|
|
154
308
|
## Changelog
|
|
155
309
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frontacles",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Front-end utilities for artisans",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -12,26 +12,26 @@
|
|
|
12
12
|
},
|
|
13
13
|
"types": "./types/index.d.ts",
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=22.12.0"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"types": "tsc && dts-bundle-generator --silent --no-banner=true -o types/index.d.ts types-transitive/index.d.ts",
|
|
19
19
|
"posttypes": "node scripts/inject-jsdoc.mjs",
|
|
20
20
|
"test": "vitest run",
|
|
21
|
-
"test:types": "
|
|
21
|
+
"test:types": "tstyche",
|
|
22
22
|
"test:ui": "vitest --ui --coverage.enabled --coverage.exclude=types",
|
|
23
|
+
"test:watch": "vitest",
|
|
23
24
|
"coverage": "vitest run --coverage --coverage.exclude=types",
|
|
24
|
-
"
|
|
25
|
-
"watch": "vitest watch",
|
|
25
|
+
"watch": "npm run test:watch",
|
|
26
26
|
"build": "echo \"Nothing to build, this command is only here to please size-limit GitHub action\" && exit 0",
|
|
27
27
|
"size": "size-limit",
|
|
28
28
|
"lint": "eslint",
|
|
29
|
-
"lint
|
|
29
|
+
"lint:fix": "eslint --fix",
|
|
30
|
+
"lint:inspect": "eslint --inspect-config"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"CHANGELOG.md",
|
|
33
34
|
"src/**/*.js",
|
|
34
|
-
"!src/**/bench",
|
|
35
35
|
"!src/**/test-utils",
|
|
36
36
|
"!src/**/*.test.js",
|
|
37
37
|
"types/index.d.ts"
|
|
@@ -57,16 +57,18 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@eslint/js": "^9.9.0",
|
|
60
|
-
"@size-limit/preset-small-lib": "^
|
|
61
|
-
"@vitest/
|
|
60
|
+
"@size-limit/preset-small-lib": "^12",
|
|
61
|
+
"@vitest/browser-playwright": "^4",
|
|
62
|
+
"@vitest/coverage-v8": "^4",
|
|
62
63
|
"@vitest/eslint-plugin": "^1.0.1",
|
|
63
|
-
"@vitest/ui": "^
|
|
64
|
+
"@vitest/ui": "^4",
|
|
64
65
|
"dts-bundle-generator": "^9.5.1",
|
|
65
66
|
"eslint": "^9.9.0",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
67
|
+
"globals": "^17",
|
|
68
|
+
"size-limit": "^12",
|
|
69
|
+
"tstyche": "^6.0.2",
|
|
68
70
|
"typescript": "^5.5.4",
|
|
69
|
-
"typescript-eslint": "^8
|
|
70
|
-
"vitest": "^
|
|
71
|
+
"typescript-eslint": "^8",
|
|
72
|
+
"vitest": "^4"
|
|
71
73
|
}
|
|
72
74
|
}
|
package/src/dom/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk update attributes of HTML element(s).
|
|
3
|
+
*
|
|
4
|
+
* Specific behaviour for the following types of values:
|
|
5
|
+
*
|
|
6
|
+
* - boolean, `null` and `undefined`:
|
|
7
|
+
* - `true` adds the attribute to the HTML, with no value (`<p hidden="">`)
|
|
8
|
+
* - `false`, `null` and `undefined` removes the attribute from the HTML
|
|
9
|
+
* - objects:
|
|
10
|
+
* - `class` will add/remove CSS classes from `{ className: state }` entries
|
|
11
|
+
* - `data` is pushed to `Element.dataset`, but a `data` property that is
|
|
12
|
+
* `null` or `undefined` removes its `data-*` attribute from the HTML:
|
|
13
|
+
* `{ data: { id: 1, enabled: false }}` gives `<p data-id="1">`
|
|
14
|
+
* - `style` is pushed to `Element.style` (inline CSS):
|
|
15
|
+
* `{ style: { color: 'red', gap: '2px' }}` gives
|
|
16
|
+
* `<p style="color: red; gap: 2px;">`
|
|
17
|
+
* - all other objects are converted the same way:
|
|
18
|
+
* `aria: { label: 'Hello!' }` gives `<p aria-label="Hello!">`
|
|
19
|
+
* - `class` can also be a string with one or more classes (space-separated),
|
|
20
|
+
* or an array of classes.
|
|
21
|
+
*
|
|
22
|
+
* @template {Element | Element[] | HTMLCollection} T
|
|
23
|
+
* @param {T} elements
|
|
24
|
+
* @param {Attributes} attributes
|
|
25
|
+
* @returns {T} The received element(s). Use it for method chaining.
|
|
26
|
+
*/
|
|
27
|
+
export const setAttributes = (elements, attributes) => {
|
|
28
|
+
const items = elements instanceof Element
|
|
29
|
+
? [elements]
|
|
30
|
+
: [...elements]
|
|
31
|
+
|
|
32
|
+
attributes = normalizeAttributes(attributes)
|
|
33
|
+
|
|
34
|
+
items.forEach(element =>
|
|
35
|
+
attributes.forEach(([name, value]) => {
|
|
36
|
+
if (value == null) {
|
|
37
|
+
return element.removeAttribute(name)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// CSS `class`, as array or object.
|
|
41
|
+
if (name == 'class') {
|
|
42
|
+
return Array.isArray(value)
|
|
43
|
+
? element.classList.add(...value)
|
|
44
|
+
: Object.entries(value).forEach(([className, classState]) =>
|
|
45
|
+
element.classList.toggle(className, classState)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Only `data` and `style` can go in this `if`.
|
|
50
|
+
if (typeof value == 'object') {
|
|
51
|
+
|
|
52
|
+
// `style` attribute (inline styles)
|
|
53
|
+
if (name == 'style') {
|
|
54
|
+
return Object.assign(element.style, value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `data-*` attributes (using `dataset`)
|
|
58
|
+
Object.assign(element.dataset, value)
|
|
59
|
+
|
|
60
|
+
return Object.entries(value).forEach(([dataKey, dataValue]) => {
|
|
61
|
+
/**
|
|
62
|
+
* Remove `data-*` prop if their value is `null` or `undefined`,
|
|
63
|
+
* because `dataset` stringify them but `setAttributes` apply
|
|
64
|
+
* the logic of regular HTML attributes to all attributes.
|
|
65
|
+
*/
|
|
66
|
+
if (dataValue == null) {
|
|
67
|
+
delete element.dataset[dataKey]
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
element.setAttribute(name, value)
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return elements
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {Record<string, any>} fnAttributes */
|
|
80
|
+
const normalizeAttributes = fnAttributes => {
|
|
81
|
+
const attributes = { ...fnAttributes }
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize object attributes.
|
|
85
|
+
* It turns `{ key: { name: value }}` into `'key-name="value"'`.
|
|
86
|
+
*/
|
|
87
|
+
Object.entries(attributes)
|
|
88
|
+
.filter(([name, value]) =>
|
|
89
|
+
value
|
|
90
|
+
&& typeof value == 'object'
|
|
91
|
+
&& !['class', 'data', 'style'].includes(name)
|
|
92
|
+
)
|
|
93
|
+
.forEach(([name, value]) => {
|
|
94
|
+
delete attributes[name]
|
|
95
|
+
|
|
96
|
+
Object.entries(value).forEach(([attrName, attrValue]) => {
|
|
97
|
+
attributes[`${name}-${attrName}`] = attrValue
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Normalize `class` attribute from string to array.
|
|
102
|
+
if (typeof attributes.class == 'string') {
|
|
103
|
+
attributes.class = attributes.class.split(' ')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Normalize boolean attributes: `true` becomes `''`, `false` becomes `null`.
|
|
107
|
+
return Object.entries(attributes)
|
|
108
|
+
.map(([name, value]) => [
|
|
109
|
+
name,
|
|
110
|
+
typeof value == 'boolean'
|
|
111
|
+
? (value ? '' : null)
|
|
112
|
+
: value // not boolean
|
|
113
|
+
])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @typedef {boolean|string|number|null|undefined} AttributePrimitive */
|
|
117
|
+
/** @typedef {AttributePrimitive | Record<string, AttributePrimitive>} AttributeValue */
|
|
118
|
+
/** @typedef {Record<string, AttributeValue>} BaseAttributes */
|
|
119
|
+
/** @typedef {{ class?: string | string[] | Record<string, boolean> }} ClassAttribute */
|
|
120
|
+
/** @typedef {{ style?: AttributePrimitive | CSSStyleDeclaration }} StyleAttribute */
|
|
121
|
+
/** @typedef {BaseAttributes | ClassAttribute | StyleAttribute} Attributes */
|
package/src/index.js
CHANGED
package/src/url/email.js
CHANGED
|
@@ -17,6 +17,9 @@ export class Email extends URL {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
+
* The email address (`username@domain.tld`) as a string.
|
|
21
|
+
*
|
|
22
|
+
* (maintainer comment)
|
|
20
23
|
* Replace the string representation of the top-level class (`URL`) to be an
|
|
21
24
|
* email address instead of `ftp://username@domain.tld`. It is needed for
|
|
22
25
|
* situation where type casting to string is involved (`console.log`…).
|
|
@@ -38,7 +41,7 @@ export class Email extends URL {
|
|
|
38
41
|
/**
|
|
39
42
|
* Whether or not an email address is parsable and valid.
|
|
40
43
|
*
|
|
41
|
-
* @param {any|Email} address
|
|
44
|
+
* @param {any|string|Email|Stringable} address
|
|
42
45
|
*/
|
|
43
46
|
static canParse(address) {
|
|
44
47
|
try {
|
|
@@ -49,3 +52,17 @@ export class Email extends URL {
|
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether or not an email address is parsable and valid.
|
|
58
|
+
*
|
|
59
|
+
* It uses WHATWG recommended RegExp: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
|
|
60
|
+
*
|
|
61
|
+
* @param {any|string|Stringable} address
|
|
62
|
+
*/
|
|
63
|
+
export const isEmail = address => /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(address)
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} Stringable
|
|
67
|
+
* @property {function(): string} toString - The object as a string.
|
|
68
|
+
*/
|
package/types/index.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export function setAttributes<T extends Element | Element[] | HTMLCollection>(elements: T, attributes: Attributes): T;
|
|
2
|
+
export type AttributePrimitive = boolean | string | number | null | undefined;
|
|
3
|
+
export type AttributeValue = AttributePrimitive | Record<string, AttributePrimitive>;
|
|
4
|
+
export type BaseAttributes = Record<string, AttributeValue>;
|
|
5
|
+
export type ClassAttribute = {
|
|
6
|
+
class?: string | string[] | Record<string, boolean>;
|
|
7
|
+
};
|
|
8
|
+
export type StyleAttribute = {
|
|
9
|
+
style?: AttributePrimitive | CSSStyleDeclaration;
|
|
10
|
+
};
|
|
11
|
+
export type Attributes = BaseAttributes | ClassAttribute | StyleAttribute;
|
|
1
12
|
export function clamp(val: number, min: number, max: number): number;
|
|
2
13
|
export function round(number: number, precision?: number): number;
|
|
3
14
|
export function capitalize(str: string): string;
|
|
@@ -17,14 +28,21 @@ export class Email extends URL {
|
|
|
17
28
|
/**
|
|
18
29
|
* Whether or not an email address is parsable and valid.
|
|
19
30
|
*
|
|
20
|
-
* @param {any|Email} address
|
|
31
|
+
* @param {any|string|Email|Stringable} address
|
|
21
32
|
*/
|
|
22
|
-
static canParse(address: any | Email): boolean;
|
|
33
|
+
static canParse(address: any | string | Email | Stringable): boolean;
|
|
23
34
|
/**
|
|
24
35
|
* @param {string|Email} address An email address like `someone@domain.tld`.
|
|
25
36
|
*/
|
|
26
37
|
constructor(address: string | Email);
|
|
27
38
|
#private;
|
|
28
39
|
}
|
|
40
|
+
export function isEmail(address: any | string | Stringable): boolean;
|
|
41
|
+
export type Stringable = {
|
|
42
|
+
/**
|
|
43
|
+
* - The object as a string.
|
|
44
|
+
*/
|
|
45
|
+
toString: () => string;
|
|
46
|
+
};
|
|
29
47
|
|
|
30
48
|
export {};
|