@fluixi/web-components 0.0.0-stage → 1.0.0-alpha.100
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/LICENSE +21 -0
- package/README.md +31 -2
- package/dist/context.cjs +162 -0
- package/dist/context.d.ts +65 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +192 -0
- package/dist/context.mjs +141 -0
- package/dist/element.cjs +847 -0
- package/dist/element.d.ts +213 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +681 -0
- package/dist/element.mjs +835 -0
- package/dist/events.cjs +35 -0
- package/dist/events.d.ts +24 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +20 -0
- package/dist/events.mjs +14 -0
- package/dist/focus.cjs +156 -0
- package/dist/focus.d.ts +34 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +185 -0
- package/dist/focus.mjs +135 -0
- package/dist/form.cjs +145 -0
- package/dist/form.d.ts +33 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +113 -0
- package/dist/form.mjs +125 -0
- package/dist/index.cjs +1202 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.mjs +1188 -0
- package/dist/props.cjs +114 -0
- package/dist/props.d.ts +93 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +99 -0
- package/dist/props.mjs +93 -0
- package/dist/registry.cjs +77 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +91 -0
- package/dist/registry.mjs +56 -0
- package/dist/styles.cjs +59 -0
- package/dist/styles.d.ts +17 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +53 -0
- package/dist/styles.mjs +38 -0
- package/dist/targets.cjs +42 -0
- package/dist/targets.d.ts +26 -0
- package/dist/targets.d.ts.map +1 -0
- package/dist/targets.js +35 -0
- package/dist/targets.mjs +21 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/types.cjs +18 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.mjs +0 -0
- package/package.json +53 -3
package/dist/props.cjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/props.ts
|
|
21
|
+
var props_exports = {};
|
|
22
|
+
__export(props_exports, {
|
|
23
|
+
createPropStates: () => createPropStates,
|
|
24
|
+
observedFrom: () => observedFrom,
|
|
25
|
+
prop: () => prop
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(props_exports);
|
|
28
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
29
|
+
var same = (a, b) => a === b;
|
|
30
|
+
function declare(initial, fromAttribute, toAttribute, options = {}) {
|
|
31
|
+
return {
|
|
32
|
+
initial,
|
|
33
|
+
attribute: options.attribute ?? "",
|
|
34
|
+
reflect: options.reflect ?? false,
|
|
35
|
+
fromAttribute,
|
|
36
|
+
toAttribute,
|
|
37
|
+
equals: options.equals ?? same
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var prop = {
|
|
41
|
+
string: (initial = "", options = {}) => declare(initial, (v) => v ?? initial, (v) => v, options),
|
|
42
|
+
number: (initial = 0, options = {}) => declare(
|
|
43
|
+
initial,
|
|
44
|
+
(v) => {
|
|
45
|
+
const n = v === null || v.trim() === "" ? NaN : Number(v);
|
|
46
|
+
return Number.isNaN(n) ? initial : n;
|
|
47
|
+
},
|
|
48
|
+
(v) => String(v),
|
|
49
|
+
options
|
|
50
|
+
),
|
|
51
|
+
boolean: (initial = false, options = {}) => declare(initial, (v) => v !== null, (v) => v ? "" : null, options),
|
|
52
|
+
/**
|
|
53
|
+
* JSON in the attribute, the parsed value in the property.
|
|
54
|
+
*
|
|
55
|
+
* Unparseable text keeps the initial value rather than throwing: the attribute is
|
|
56
|
+
* author-supplied and often mid-edit, and an exception in `attributeChangedCallback`
|
|
57
|
+
* leaves the element half-upgraded.
|
|
58
|
+
*/
|
|
59
|
+
object: (initial, options = {}) => declare(
|
|
60
|
+
initial,
|
|
61
|
+
(v) => {
|
|
62
|
+
if (v === null) return initial;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(v);
|
|
65
|
+
} catch {
|
|
66
|
+
return initial;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
(v) => v === void 0 ? null : JSON.stringify(v),
|
|
70
|
+
options
|
|
71
|
+
),
|
|
72
|
+
/**
|
|
73
|
+
* Your own conversion, for a type the built-ins don't cover: a date, a list, a unit.
|
|
74
|
+
*
|
|
75
|
+
* `fromAttribute` only sees a present attribute; removing it goes back to `initial`, so a
|
|
76
|
+
* converter never has to handle `null`. It may return `initial` for text it can't read.
|
|
77
|
+
* `toAttribute` is needed only to reflect, and returning `null` removes the attribute.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* due: prop.custom(new Date(0), {
|
|
82
|
+
* fromAttribute: (v) => new Date(v),
|
|
83
|
+
* toAttribute: (d) => d.toISOString(),
|
|
84
|
+
* }, { equals: (a, b) => a.getTime() === b.getTime() }),
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
custom: (initial, converter, options = {}) => declare(
|
|
88
|
+
initial,
|
|
89
|
+
(v) => v === null ? initial : converter.fromAttribute(v),
|
|
90
|
+
converter.toAttribute ?? ((v) => v == null ? null : String(v)),
|
|
91
|
+
options
|
|
92
|
+
),
|
|
93
|
+
/** No attribute at all. For a value only a property assignment can carry. */
|
|
94
|
+
any: (initial, options = {}) => declare(initial, () => initial, () => null, { ...options, attribute: false })
|
|
95
|
+
};
|
|
96
|
+
function createPropStates(schema) {
|
|
97
|
+
const states = {};
|
|
98
|
+
for (const [name, declaration] of Object.entries(schema)) {
|
|
99
|
+
const value = (0, import_signal.signal)(declaration.initial, { equals: declaration.equals, name });
|
|
100
|
+
states[name] = {
|
|
101
|
+
read: value,
|
|
102
|
+
// `.set` reads a function argument as an updater and calls it. A property holding
|
|
103
|
+
// a callback is ordinary for a custom element, so it is wrapped: the value
|
|
104
|
+
// arrives as the return of an updater that ignores the previous one.
|
|
105
|
+
write: (next) => void (typeof next === "function" ? value.set(() => next) : value.set(next)),
|
|
106
|
+
declaration,
|
|
107
|
+
attribute: declaration.attribute === "" ? name.toLowerCase() : declaration.attribute
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return states;
|
|
111
|
+
}
|
|
112
|
+
function observedFrom(schema) {
|
|
113
|
+
return Object.entries(schema).map(([name, d]) => d.attribute === "" ? name.toLowerCase() : d.attribute).filter((a) => a !== false);
|
|
114
|
+
}
|
package/dist/props.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Properties, and the attributes that mirror them.
|
|
3
|
+
*
|
|
4
|
+
* A custom element has two ways in. A property assignment (`el.count = 3`) carries a
|
|
5
|
+
* real value and is what a framework binding uses. An attribute (`count="3"`) carries
|
|
6
|
+
* a string and is what HTML and a server render can express. Both have to land on the
|
|
7
|
+
* same signal or the two go out of sync.
|
|
8
|
+
*
|
|
9
|
+
* Each declaration owns the conversion in one place, so the element never guesses.
|
|
10
|
+
* `attribute: false` opts a property out of HTML entirely, which is the right answer
|
|
11
|
+
* for anything a string cannot carry: a function, a Date, a large object.
|
|
12
|
+
*/
|
|
13
|
+
import { type Accessor } from '@fluixi/reactive/signal';
|
|
14
|
+
export interface PropDeclaration<T> {
|
|
15
|
+
/** Value before anything sets one. */
|
|
16
|
+
readonly initial: T;
|
|
17
|
+
/**
|
|
18
|
+
* The attribute this mirrors, or false for a property with no HTML form.
|
|
19
|
+
* Defaults to the property name lowercased.
|
|
20
|
+
*/
|
|
21
|
+
readonly attribute: string | false;
|
|
22
|
+
/** Write the property back out to the attribute whenever it changes. */
|
|
23
|
+
readonly reflect: boolean;
|
|
24
|
+
/** Attribute string to property value. `null` means the attribute was removed. */
|
|
25
|
+
fromAttribute(value: string | null): T;
|
|
26
|
+
/** Property value to attribute string. `null` removes the attribute. */
|
|
27
|
+
toAttribute(value: T): string | null;
|
|
28
|
+
/** Skip a write that would not change anything, so a reflect loop terminates. */
|
|
29
|
+
equals(a: T, b: T): boolean;
|
|
30
|
+
}
|
|
31
|
+
export type PropsSchema = Record<string, PropDeclaration<never>>;
|
|
32
|
+
/** The reactive accessors an element exposes for its declared properties. */
|
|
33
|
+
export type PropAccessors<S extends Record<string, PropDeclaration<unknown>>> = {
|
|
34
|
+
readonly [K in keyof S]: Accessor<S[K] extends PropDeclaration<infer T> ? T : never>;
|
|
35
|
+
};
|
|
36
|
+
export interface PropOptions<T> {
|
|
37
|
+
attribute?: string | false;
|
|
38
|
+
reflect?: boolean;
|
|
39
|
+
equals?: (a: T, b: T) => boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The built-in conversions.
|
|
43
|
+
*
|
|
44
|
+
* `boolean` follows HTML rather than JSON: presence is true, absence is false, and the
|
|
45
|
+
* value is ignored, so `disabled=""` and `disabled="false"` are both true. Anything
|
|
46
|
+
* else would disagree with every native element on the page.
|
|
47
|
+
*/
|
|
48
|
+
export declare const prop: {
|
|
49
|
+
string: (initial?: string, options?: PropOptions<string>) => PropDeclaration<string>;
|
|
50
|
+
number: (initial?: number, options?: PropOptions<number>) => PropDeclaration<number>;
|
|
51
|
+
boolean: (initial?: boolean, options?: PropOptions<boolean>) => PropDeclaration<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* JSON in the attribute, the parsed value in the property.
|
|
54
|
+
*
|
|
55
|
+
* Unparseable text keeps the initial value rather than throwing: the attribute is
|
|
56
|
+
* author-supplied and often mid-edit, and an exception in `attributeChangedCallback`
|
|
57
|
+
* leaves the element half-upgraded.
|
|
58
|
+
*/
|
|
59
|
+
object: <T>(initial: T, options?: PropOptions<T>) => PropDeclaration<T>;
|
|
60
|
+
/**
|
|
61
|
+
* Your own conversion, for a type the built-ins don't cover: a date, a list, a unit.
|
|
62
|
+
*
|
|
63
|
+
* `fromAttribute` only sees a present attribute; removing it goes back to `initial`, so a
|
|
64
|
+
* converter never has to handle `null`. It may return `initial` for text it can't read.
|
|
65
|
+
* `toAttribute` is needed only to reflect, and returning `null` removes the attribute.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* due: prop.custom(new Date(0), {
|
|
70
|
+
* fromAttribute: (v) => new Date(v),
|
|
71
|
+
* toAttribute: (d) => d.toISOString(),
|
|
72
|
+
* }, { equals: (a, b) => a.getTime() === b.getTime() }),
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
custom: <T>(initial: T, converter: {
|
|
76
|
+
fromAttribute: (value: string) => T;
|
|
77
|
+
toAttribute?: (value: T) => string | null;
|
|
78
|
+
}, options?: PropOptions<T>) => PropDeclaration<T>;
|
|
79
|
+
/** No attribute at all. For a value only a property assignment can carry. */
|
|
80
|
+
any: <T>(initial: T, options?: Omit<PropOptions<T>, "attribute" | "reflect">) => PropDeclaration<T>;
|
|
81
|
+
};
|
|
82
|
+
/** One property's signal plus the declaration that drives its attribute. */
|
|
83
|
+
export interface PropState<T> {
|
|
84
|
+
read: Accessor<T>;
|
|
85
|
+
write(value: T): void;
|
|
86
|
+
declaration: PropDeclaration<T>;
|
|
87
|
+
/** The attribute name, resolved from the property name when it was left blank. */
|
|
88
|
+
attribute: string | false;
|
|
89
|
+
}
|
|
90
|
+
export declare function createPropStates(schema: Record<string, PropDeclaration<unknown>>): Record<string, PropState<unknown>>;
|
|
91
|
+
/** The attribute names an element has to observe, derived from its schema. */
|
|
92
|
+
export declare function observedFrom(schema: Record<string, PropDeclaration<unknown>>): string[];
|
|
93
|
+
//# sourceMappingURL=props.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEhE,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,kFAAkF;IAClF,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC;IACvC,wEAAwE;IACxE,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC;IACrC,iFAAiF;IACjF,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;AAEjE,6EAA6E;AAC7E,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,IAAI;IAC9E,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,eAAe,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;CACrF,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;CAClC;AAoBD;;;;;;GAMG;AACH,eAAO,MAAM,IAAI;yCACiB,WAAW,CAAC,MAAM,CAAC;yCAGpB,WAAW,CAAC,MAAM,CAAC;2CAad,WAAW,CAAC,OAAO,CAAC;IAGxD;;;;;;OAMG;aACM,CAAC,WAAW,CAAC,YAAW,WAAW,CAAC,CAAC,CAAC;IAe/C;;;;;;;;;;;;;;OAcG;aACM,CAAC,WACC,CAAC,aACC;QAAE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;QAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;KAAE,YACpF,WAAW,CAAC,CAAC,CAAC;IASzB,6EAA6E;UACvE,CAAC,WAAW,CAAC,YAAW,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAE5E,CAAC;AAEF,4EAA4E;AAC5E,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAClB,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IAChC,kFAAkF;IAClF,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,GAC/C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,CAiBpC;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC,GAAG,MAAM,EAAE,CAIvF"}
|
package/dist/props.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Properties, and the attributes that mirror them.
|
|
3
|
+
*
|
|
4
|
+
* A custom element has two ways in. A property assignment (`el.count = 3`) carries a
|
|
5
|
+
* real value and is what a framework binding uses. An attribute (`count="3"`) carries
|
|
6
|
+
* a string and is what HTML and a server render can express. Both have to land on the
|
|
7
|
+
* same signal or the two go out of sync.
|
|
8
|
+
*
|
|
9
|
+
* Each declaration owns the conversion in one place, so the element never guesses.
|
|
10
|
+
* `attribute: false` opts a property out of HTML entirely, which is the right answer
|
|
11
|
+
* for anything a string cannot carry: a function, a Date, a large object.
|
|
12
|
+
*/
|
|
13
|
+
import { signal } from '@fluixi/reactive/signal';
|
|
14
|
+
const same = (a, b) => a === b;
|
|
15
|
+
function declare(initial, fromAttribute, toAttribute, options = {}) {
|
|
16
|
+
return {
|
|
17
|
+
initial,
|
|
18
|
+
attribute: options.attribute ?? '',
|
|
19
|
+
reflect: options.reflect ?? false,
|
|
20
|
+
fromAttribute,
|
|
21
|
+
toAttribute,
|
|
22
|
+
equals: options.equals ?? same,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The built-in conversions.
|
|
27
|
+
*
|
|
28
|
+
* `boolean` follows HTML rather than JSON: presence is true, absence is false, and the
|
|
29
|
+
* value is ignored, so `disabled=""` and `disabled="false"` are both true. Anything
|
|
30
|
+
* else would disagree with every native element on the page.
|
|
31
|
+
*/
|
|
32
|
+
export const prop = {
|
|
33
|
+
string: (initial = '', options = {}) => declare(initial, (v) => v ?? initial, (v) => v, options),
|
|
34
|
+
number: (initial = 0, options = {}) => declare(initial, (v) => {
|
|
35
|
+
// Unparseable text keeps the initial value, as `object` does: NaN would pass every
|
|
36
|
+
// type check and then poison arithmetic downstream.
|
|
37
|
+
const n = v === null || v.trim() === '' ? NaN : Number(v);
|
|
38
|
+
return Number.isNaN(n) ? initial : n;
|
|
39
|
+
}, (v) => String(v), options),
|
|
40
|
+
boolean: (initial = false, options = {}) => declare(initial, (v) => v !== null, (v) => (v ? '' : null), options),
|
|
41
|
+
/**
|
|
42
|
+
* JSON in the attribute, the parsed value in the property.
|
|
43
|
+
*
|
|
44
|
+
* Unparseable text keeps the initial value rather than throwing: the attribute is
|
|
45
|
+
* author-supplied and often mid-edit, and an exception in `attributeChangedCallback`
|
|
46
|
+
* leaves the element half-upgraded.
|
|
47
|
+
*/
|
|
48
|
+
object: (initial, options = {}) => declare(initial, (v) => {
|
|
49
|
+
if (v === null)
|
|
50
|
+
return initial;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(v);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return initial;
|
|
56
|
+
}
|
|
57
|
+
}, (v) => (v === undefined ? null : JSON.stringify(v)), options),
|
|
58
|
+
/**
|
|
59
|
+
* Your own conversion, for a type the built-ins don't cover: a date, a list, a unit.
|
|
60
|
+
*
|
|
61
|
+
* `fromAttribute` only sees a present attribute; removing it goes back to `initial`, so a
|
|
62
|
+
* converter never has to handle `null`. It may return `initial` for text it can't read.
|
|
63
|
+
* `toAttribute` is needed only to reflect, and returning `null` removes the attribute.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* due: prop.custom(new Date(0), {
|
|
68
|
+
* fromAttribute: (v) => new Date(v),
|
|
69
|
+
* toAttribute: (d) => d.toISOString(),
|
|
70
|
+
* }, { equals: (a, b) => a.getTime() === b.getTime() }),
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
custom: (initial, converter, options = {}) => declare(initial, (v) => (v === null ? initial : converter.fromAttribute(v)), converter.toAttribute ?? ((v) => (v == null ? null : String(v))), options),
|
|
74
|
+
/** No attribute at all. For a value only a property assignment can carry. */
|
|
75
|
+
any: (initial, options = {}) => declare(initial, () => initial, () => null, { ...options, attribute: false }),
|
|
76
|
+
};
|
|
77
|
+
export function createPropStates(schema) {
|
|
78
|
+
const states = {};
|
|
79
|
+
for (const [name, declaration] of Object.entries(schema)) {
|
|
80
|
+
// Named, so the devtools graph shows `step` rather than an anonymous signal.
|
|
81
|
+
const value = signal(declaration.initial, { equals: declaration.equals, name });
|
|
82
|
+
states[name] = {
|
|
83
|
+
read: value,
|
|
84
|
+
// `.set` reads a function argument as an updater and calls it. A property holding
|
|
85
|
+
// a callback is ordinary for a custom element, so it is wrapped: the value
|
|
86
|
+
// arrives as the return of an updater that ignores the previous one.
|
|
87
|
+
write: (next) => void (typeof next === 'function' ? value.set(() => next) : value.set(next)),
|
|
88
|
+
declaration,
|
|
89
|
+
attribute: declaration.attribute === '' ? name.toLowerCase() : declaration.attribute,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return states;
|
|
93
|
+
}
|
|
94
|
+
/** The attribute names an element has to observe, derived from its schema. */
|
|
95
|
+
export function observedFrom(schema) {
|
|
96
|
+
return Object.entries(schema)
|
|
97
|
+
.map(([name, d]) => (d.attribute === '' ? name.toLowerCase() : d.attribute))
|
|
98
|
+
.filter((a) => a !== false);
|
|
99
|
+
}
|
package/dist/props.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/props.ts
|
|
2
|
+
import { signal } from "@fluixi/reactive/signal";
|
|
3
|
+
var same = (a, b) => a === b;
|
|
4
|
+
function declare(initial, fromAttribute, toAttribute, options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
initial,
|
|
7
|
+
attribute: options.attribute ?? "",
|
|
8
|
+
reflect: options.reflect ?? false,
|
|
9
|
+
fromAttribute,
|
|
10
|
+
toAttribute,
|
|
11
|
+
equals: options.equals ?? same
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var prop = {
|
|
15
|
+
string: (initial = "", options = {}) => declare(initial, (v) => v ?? initial, (v) => v, options),
|
|
16
|
+
number: (initial = 0, options = {}) => declare(
|
|
17
|
+
initial,
|
|
18
|
+
(v) => {
|
|
19
|
+
const n = v === null || v.trim() === "" ? NaN : Number(v);
|
|
20
|
+
return Number.isNaN(n) ? initial : n;
|
|
21
|
+
},
|
|
22
|
+
(v) => String(v),
|
|
23
|
+
options
|
|
24
|
+
),
|
|
25
|
+
boolean: (initial = false, options = {}) => declare(initial, (v) => v !== null, (v) => v ? "" : null, options),
|
|
26
|
+
/**
|
|
27
|
+
* JSON in the attribute, the parsed value in the property.
|
|
28
|
+
*
|
|
29
|
+
* Unparseable text keeps the initial value rather than throwing: the attribute is
|
|
30
|
+
* author-supplied and often mid-edit, and an exception in `attributeChangedCallback`
|
|
31
|
+
* leaves the element half-upgraded.
|
|
32
|
+
*/
|
|
33
|
+
object: (initial, options = {}) => declare(
|
|
34
|
+
initial,
|
|
35
|
+
(v) => {
|
|
36
|
+
if (v === null) return initial;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(v);
|
|
39
|
+
} catch {
|
|
40
|
+
return initial;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
(v) => v === void 0 ? null : JSON.stringify(v),
|
|
44
|
+
options
|
|
45
|
+
),
|
|
46
|
+
/**
|
|
47
|
+
* Your own conversion, for a type the built-ins don't cover: a date, a list, a unit.
|
|
48
|
+
*
|
|
49
|
+
* `fromAttribute` only sees a present attribute; removing it goes back to `initial`, so a
|
|
50
|
+
* converter never has to handle `null`. It may return `initial` for text it can't read.
|
|
51
|
+
* `toAttribute` is needed only to reflect, and returning `null` removes the attribute.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* due: prop.custom(new Date(0), {
|
|
56
|
+
* fromAttribute: (v) => new Date(v),
|
|
57
|
+
* toAttribute: (d) => d.toISOString(),
|
|
58
|
+
* }, { equals: (a, b) => a.getTime() === b.getTime() }),
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
custom: (initial, converter, options = {}) => declare(
|
|
62
|
+
initial,
|
|
63
|
+
(v) => v === null ? initial : converter.fromAttribute(v),
|
|
64
|
+
converter.toAttribute ?? ((v) => v == null ? null : String(v)),
|
|
65
|
+
options
|
|
66
|
+
),
|
|
67
|
+
/** No attribute at all. For a value only a property assignment can carry. */
|
|
68
|
+
any: (initial, options = {}) => declare(initial, () => initial, () => null, { ...options, attribute: false })
|
|
69
|
+
};
|
|
70
|
+
function createPropStates(schema) {
|
|
71
|
+
const states = {};
|
|
72
|
+
for (const [name, declaration] of Object.entries(schema)) {
|
|
73
|
+
const value = signal(declaration.initial, { equals: declaration.equals, name });
|
|
74
|
+
states[name] = {
|
|
75
|
+
read: value,
|
|
76
|
+
// `.set` reads a function argument as an updater and calls it. A property holding
|
|
77
|
+
// a callback is ordinary for a custom element, so it is wrapped: the value
|
|
78
|
+
// arrives as the return of an updater that ignores the previous one.
|
|
79
|
+
write: (next) => void (typeof next === "function" ? value.set(() => next) : value.set(next)),
|
|
80
|
+
declaration,
|
|
81
|
+
attribute: declaration.attribute === "" ? name.toLowerCase() : declaration.attribute
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return states;
|
|
85
|
+
}
|
|
86
|
+
function observedFrom(schema) {
|
|
87
|
+
return Object.entries(schema).map(([name, d]) => d.attribute === "" ? name.toLowerCase() : d.attribute).filter((a) => a !== false);
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
createPropStates,
|
|
91
|
+
observedFrom,
|
|
92
|
+
prop
|
|
93
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/registry.ts
|
|
21
|
+
var registry_exports = {};
|
|
22
|
+
__export(registry_exports, {
|
|
23
|
+
defineCustomElement: () => defineCustomElement,
|
|
24
|
+
defineWhenUsed: () => defineWhenUsed,
|
|
25
|
+
isDefined: () => isDefined,
|
|
26
|
+
resetRegistryTracking: () => resetRegistryTracking
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(registry_exports);
|
|
29
|
+
var import_dom = require("@fluixi/dom");
|
|
30
|
+
var defined = /* @__PURE__ */ new Map();
|
|
31
|
+
function registry() {
|
|
32
|
+
return typeof customElements === "undefined" ? null : customElements;
|
|
33
|
+
}
|
|
34
|
+
function defineCustomElement(name, constructor, options = {}) {
|
|
35
|
+
if ((0, import_dom.isServer)() && typeof HTMLElement !== "undefined") return constructor;
|
|
36
|
+
const registered = defined.get(name);
|
|
37
|
+
if (registered) return registered;
|
|
38
|
+
const ce = registry();
|
|
39
|
+
if (!ce) return constructor;
|
|
40
|
+
const existing = ce.get(name);
|
|
41
|
+
if (existing) {
|
|
42
|
+
defined.set(name, existing);
|
|
43
|
+
return existing;
|
|
44
|
+
}
|
|
45
|
+
ce.define(name, constructor, options.extends ? { extends: options.extends } : void 0);
|
|
46
|
+
defined.set(name, constructor);
|
|
47
|
+
return constructor;
|
|
48
|
+
}
|
|
49
|
+
function isDefined(name) {
|
|
50
|
+
return defined.has(name) || registry()?.get(name) !== void 0;
|
|
51
|
+
}
|
|
52
|
+
function defineWhenUsed(name, load, options = {}) {
|
|
53
|
+
if ((0, import_dom.isServer)() || typeof document === "undefined") return;
|
|
54
|
+
let started = false;
|
|
55
|
+
const start = async () => {
|
|
56
|
+
if (started) return;
|
|
57
|
+
started = true;
|
|
58
|
+
defineCustomElement(name, await load(), options);
|
|
59
|
+
};
|
|
60
|
+
if (document.querySelector(name)) {
|
|
61
|
+
void start();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (typeof MutationObserver === "undefined") {
|
|
65
|
+
void start();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const observer = new MutationObserver(() => {
|
|
69
|
+
if (!document.querySelector(name)) return;
|
|
70
|
+
observer.disconnect();
|
|
71
|
+
void start();
|
|
72
|
+
});
|
|
73
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
74
|
+
}
|
|
75
|
+
function resetRegistryTracking() {
|
|
76
|
+
defined.clear();
|
|
77
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface DefineOptions {
|
|
2
|
+
/** Extend a built-in, as `is="..."`. Safari does not implement this. */
|
|
3
|
+
extends?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Register a constructor under a name, at most once.
|
|
7
|
+
*
|
|
8
|
+
* Returns the constructor actually registered, which is the previously registered one
|
|
9
|
+
* when the name is already taken. That matters for a standalone bundle: two copies of
|
|
10
|
+
* the same component on a page should agree on which class the tag upgrades to, and
|
|
11
|
+
* the first to load wins.
|
|
12
|
+
*/
|
|
13
|
+
export declare function defineCustomElement(name: string, constructor: CustomElementConstructor, options?: DefineOptions): CustomElementConstructor;
|
|
14
|
+
/** Whether a name has been claimed, by this module or anyone else. */
|
|
15
|
+
export declare function isDefined(name: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Register only once the tag is actually in the document.
|
|
18
|
+
*
|
|
19
|
+
* A component that costs something to load should not cost it on a page that never
|
|
20
|
+
* uses the tag. The observer disconnects after the first match, so the scan is paid
|
|
21
|
+
* for once.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defineWhenUsed(name: string, load: () => CustomElementConstructor | Promise<CustomElementConstructor>, options?: DefineOptions): void;
|
|
24
|
+
/** Test seam: forget what this module has registered. The registry itself is permanent. */
|
|
25
|
+
export declare function resetRegistryTracking(): void;
|
|
26
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AASD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,wBAAwB,EACrC,OAAO,GAAE,aAAkB,GAC1B,wBAAwB,CAuB1B;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,CAAC,EACxE,OAAO,GAAE,aAAkB,GAC1B,IAAI,CA0BN;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration, which is global and happens once.
|
|
3
|
+
*
|
|
4
|
+
* `customElements.define` throws on a repeated name, and a module that registers on
|
|
5
|
+
* import gets evaluated more than once more often than people expect: two bundles on
|
|
6
|
+
* a page, a dev server round trip, a test file importing the module twice. The throw
|
|
7
|
+
* is a `NotSupportedError` that surfaces far from its cause.
|
|
8
|
+
*
|
|
9
|
+
* Registration is also the one part of this package that cannot run on a server. The
|
|
10
|
+
* guard is `isServer()` from @fluixi/dom rather than a `typeof window` test, because
|
|
11
|
+
* that flag is a realm singleton the server renderer sets. Four copies of a local
|
|
12
|
+
* `isServer` was the alpha.83 SSR bug, and this package does not add a fifth.
|
|
13
|
+
*/
|
|
14
|
+
import { isServer } from '@fluixi/dom';
|
|
15
|
+
/** Names this module has defined, so a second call is a no-op rather than a throw. */
|
|
16
|
+
const defined = new Map();
|
|
17
|
+
function registry() {
|
|
18
|
+
return typeof customElements === 'undefined' ? null : customElements;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Register a constructor under a name, at most once.
|
|
22
|
+
*
|
|
23
|
+
* Returns the constructor actually registered, which is the previously registered one
|
|
24
|
+
* when the name is already taken. That matters for a standalone bundle: two copies of
|
|
25
|
+
* the same component on a page should agree on which class the tag upgrades to, and
|
|
26
|
+
* the first to load wins.
|
|
27
|
+
*/
|
|
28
|
+
export function defineCustomElement(name, constructor, options = {}) {
|
|
29
|
+
// A server render in a browser-like realm (a jsdom test) must not define for real. On Node
|
|
30
|
+
// the stand-in registry takes it, so the server renderer can find the element by its tag.
|
|
31
|
+
if (isServer() && typeof HTMLElement !== 'undefined')
|
|
32
|
+
return constructor;
|
|
33
|
+
const registered = defined.get(name);
|
|
34
|
+
if (registered)
|
|
35
|
+
return registered;
|
|
36
|
+
const ce = registry();
|
|
37
|
+
// No registry means a non-browser realm that is not the server renderer either,
|
|
38
|
+
// such as a worker. Nothing to register into, and nothing to warn about.
|
|
39
|
+
if (!ce)
|
|
40
|
+
return constructor;
|
|
41
|
+
const existing = ce.get(name);
|
|
42
|
+
if (existing) {
|
|
43
|
+
// Another bundle got here first. Remember it so this module stops asking.
|
|
44
|
+
defined.set(name, existing);
|
|
45
|
+
return existing;
|
|
46
|
+
}
|
|
47
|
+
ce.define(name, constructor, options.extends ? { extends: options.extends } : undefined);
|
|
48
|
+
defined.set(name, constructor);
|
|
49
|
+
return constructor;
|
|
50
|
+
}
|
|
51
|
+
/** Whether a name has been claimed, by this module or anyone else. */
|
|
52
|
+
export function isDefined(name) {
|
|
53
|
+
return defined.has(name) || registry()?.get(name) !== undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register only once the tag is actually in the document.
|
|
57
|
+
*
|
|
58
|
+
* A component that costs something to load should not cost it on a page that never
|
|
59
|
+
* uses the tag. The observer disconnects after the first match, so the scan is paid
|
|
60
|
+
* for once.
|
|
61
|
+
*/
|
|
62
|
+
export function defineWhenUsed(name, load, options = {}) {
|
|
63
|
+
if (isServer() || typeof document === 'undefined')
|
|
64
|
+
return;
|
|
65
|
+
let started = false;
|
|
66
|
+
const start = async () => {
|
|
67
|
+
if (started)
|
|
68
|
+
return;
|
|
69
|
+
started = true;
|
|
70
|
+
defineCustomElement(name, await load(), options);
|
|
71
|
+
};
|
|
72
|
+
if (document.querySelector(name)) {
|
|
73
|
+
void start();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (typeof MutationObserver === 'undefined') {
|
|
77
|
+
void start();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const observer = new MutationObserver(() => {
|
|
81
|
+
if (!document.querySelector(name))
|
|
82
|
+
return;
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
void start();
|
|
85
|
+
});
|
|
86
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
87
|
+
}
|
|
88
|
+
/** Test seam: forget what this module has registered. The registry itself is permanent. */
|
|
89
|
+
export function resetRegistryTracking() {
|
|
90
|
+
defined.clear();
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
import { isServer } from "@fluixi/dom";
|
|
3
|
+
var defined = /* @__PURE__ */ new Map();
|
|
4
|
+
function registry() {
|
|
5
|
+
return typeof customElements === "undefined" ? null : customElements;
|
|
6
|
+
}
|
|
7
|
+
function defineCustomElement(name, constructor, options = {}) {
|
|
8
|
+
if (isServer() && typeof HTMLElement !== "undefined") return constructor;
|
|
9
|
+
const registered = defined.get(name);
|
|
10
|
+
if (registered) return registered;
|
|
11
|
+
const ce = registry();
|
|
12
|
+
if (!ce) return constructor;
|
|
13
|
+
const existing = ce.get(name);
|
|
14
|
+
if (existing) {
|
|
15
|
+
defined.set(name, existing);
|
|
16
|
+
return existing;
|
|
17
|
+
}
|
|
18
|
+
ce.define(name, constructor, options.extends ? { extends: options.extends } : void 0);
|
|
19
|
+
defined.set(name, constructor);
|
|
20
|
+
return constructor;
|
|
21
|
+
}
|
|
22
|
+
function isDefined(name) {
|
|
23
|
+
return defined.has(name) || registry()?.get(name) !== void 0;
|
|
24
|
+
}
|
|
25
|
+
function defineWhenUsed(name, load, options = {}) {
|
|
26
|
+
if (isServer() || typeof document === "undefined") return;
|
|
27
|
+
let started = false;
|
|
28
|
+
const start = async () => {
|
|
29
|
+
if (started) return;
|
|
30
|
+
started = true;
|
|
31
|
+
defineCustomElement(name, await load(), options);
|
|
32
|
+
};
|
|
33
|
+
if (document.querySelector(name)) {
|
|
34
|
+
void start();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (typeof MutationObserver === "undefined") {
|
|
38
|
+
void start();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const observer = new MutationObserver(() => {
|
|
42
|
+
if (!document.querySelector(name)) return;
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
void start();
|
|
45
|
+
});
|
|
46
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
47
|
+
}
|
|
48
|
+
function resetRegistryTracking() {
|
|
49
|
+
defined.clear();
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
defineCustomElement,
|
|
53
|
+
defineWhenUsed,
|
|
54
|
+
isDefined,
|
|
55
|
+
resetRegistryTracking
|
|
56
|
+
};
|