@studiometa/js-toolkit 3.6.2 → 3.8.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.
@@ -8,9 +8,32 @@ export interface WithGroupProps extends BaseProps {
8
8
  export interface WithGroupInterface extends BaseInterface {
9
9
  get $group(): Set<Base>;
10
10
  }
11
+ /**
12
+ * Options for the `withGroup` decorator.
13
+ */
14
+ export interface WithGroupOptions<T extends Base = Base, Scope extends object = object> {
15
+ /**
16
+ * Resolve the scope an instance belongs to. Return `undefined` to fall back
17
+ * to the global (unscoped) registry.
18
+ */
19
+ getScope?: (instance: T) => Scope | undefined;
20
+ /**
21
+ * Resolve the group name of an instance, optionally using its resolved scope.
22
+ * Defaults to `instance.$options.group`.
23
+ */
24
+ getGroup?: (instance: T, scope: Scope | undefined) => string;
25
+ }
26
+ /**
27
+ * Enumerate the scoped groups registered for a given scope.
28
+ *
29
+ * Returns the live `Map` of `${namespace}${group}` keys to their members set
30
+ * for the given scope, or an empty `Map` for unknown scopes. Unknown scopes are
31
+ * NOT registered as a side effect of reading.
32
+ */
33
+ export declare function getScopedGroups(scope: object): Map<string, Set<Base>>;
11
34
  /**
12
35
  * Add group features to easily bind multiple components together.
13
36
  *
14
37
  * @link https://js-toolkit.studiometa.dev/api/decorators/withGroup.html
15
38
  */
16
- export declare function withGroup<S extends Base = Base>(BaseClass: typeof Base, namespace?: string): BaseDecorator<WithGroupInterface, S, WithGroupProps>;
39
+ export declare function withGroup<S extends Base = Base, Scope extends object = object>(BaseClass: typeof Base, namespace?: string, options?: WithGroupOptions<S, Scope>): BaseDecorator<WithGroupInterface, S, WithGroupProps>;
@@ -1,7 +1,30 @@
1
1
  function groups() {
2
2
  return globalThis.__JS_TOOLKIT_GROUPS__ ??= /* @__PURE__ */ new Map();
3
3
  }
4
- function withGroup(BaseClass, namespace = "") {
4
+ const scopedGroups = /* @__PURE__ */ new WeakMap();
5
+ function getScopedGroups(scope) {
6
+ return scopedGroups.get(scope) ?? /* @__PURE__ */ new Map();
7
+ }
8
+ function resolveGroupSet(scope, key) {
9
+ if (scope) {
10
+ let map = scopedGroups.get(scope);
11
+ if (!map) {
12
+ map = /* @__PURE__ */ new Map();
13
+ scopedGroups.set(scope, map);
14
+ }
15
+ let set = map.get(key);
16
+ if (!set) {
17
+ set = /* @__PURE__ */ new Set();
18
+ map.set(key, set);
19
+ }
20
+ return set;
21
+ }
22
+ const registry = groups();
23
+ return registry.get(key) ?? registry.set(key, /* @__PURE__ */ new Set()).get(key);
24
+ }
25
+ function withGroup(BaseClass, namespace = "", options = {}) {
26
+ const getScope = options.getScope;
27
+ const getGroup = options.getGroup ?? ((instance) => instance.$options.group);
5
28
  return class WithGroup extends BaseClass {
6
29
  static config = {
7
30
  ...BaseClass.config,
@@ -9,12 +32,27 @@ function withGroup(BaseClass, namespace = "") {
9
32
  group: String
10
33
  }
11
34
  };
35
+ /**
36
+ * The members set remembered at mount time, if any.
37
+ *
38
+ * @private
39
+ */
40
+ __groupSnapshot = void 0;
41
+ /**
42
+ * Resolve the members set for the current scope and group.
43
+ *
44
+ * @private
45
+ */
46
+ __resolveGroup() {
47
+ const scope = getScope?.(this);
48
+ const group = `${namespace}${getGroup(this, scope)}`;
49
+ return resolveGroupSet(scope, group);
50
+ }
12
51
  /**
13
52
  * Get the group set.
14
53
  */
15
54
  get $group() {
16
- const group = `${namespace}${this.$options.group}`;
17
- const instances = groups().get(group) ?? groups().set(group, /* @__PURE__ */ new Set()).get(group);
55
+ const instances = this.__groupSnapshot ?? this.__resolveGroup();
18
56
  for (const instance of instances) {
19
57
  if (!instance.$el.isConnected) {
20
58
  instances.delete(instance);
@@ -25,15 +63,19 @@ function withGroup(BaseClass, namespace = "") {
25
63
  constructor(element) {
26
64
  super(element);
27
65
  this.$on("mounted", () => {
28
- this.$group.add(this);
66
+ const instances = this.$group;
67
+ instances.add(this);
68
+ this.__groupSnapshot = instances;
29
69
  });
30
70
  this.$on("destroyed", () => {
31
- this.$group.delete(this);
71
+ this.__groupSnapshot?.delete(this);
72
+ this.__groupSnapshot = void 0;
32
73
  });
33
74
  }
34
75
  };
35
76
  }
36
77
  export {
78
+ getScopedGroups,
37
79
  withGroup
38
80
  };
39
81
  //# sourceMappingURL=withGroup.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../packages/js-toolkit/decorators/withGroup.ts"],
4
- "sourcesContent": ["import type { BaseDecorator, BaseInterface } from '../Base/types.js';\nimport type { Base, BaseProps, BaseConfig } from '../Base/index.js';\n\nexport interface WithGroupProps extends BaseProps {\n $options: {\n group: string;\n };\n}\n\nexport interface WithGroupInterface extends BaseInterface {\n get $group(): Set<Base>;\n}\n\n/**\n * Get global groups map.\n */\nfunction groups<T extends Base = Base>(): Map<string, Set<T>> {\n return (globalThis.__JS_TOOLKIT_GROUPS__ ??= new Map<string, Set<T>>());\n}\n\n/**\n * Add group features to easily bind multiple components together.\n *\n * @link https://js-toolkit.studiometa.dev/api/decorators/withGroup.html\n */\nexport function withGroup<S extends Base = Base>(\n BaseClass: typeof Base,\n namespace = '',\n): BaseDecorator<WithGroupInterface, S, WithGroupProps> {\n // @ts-expect-error Decorators can not be typed.\n return class WithGroup<T extends BaseProps = BaseProps> extends BaseClass<T & WithGroupProps> {\n static config: BaseConfig = {\n ...BaseClass.config,\n options: {\n group: String,\n },\n };\n\n /**\n * Get the group set.\n */\n get $group() {\n const group = `${namespace}${this.$options.group}`;\n const instances =\n groups<this>().get(group) ?? groups<this>().set(group, new Set()).get(group);\n\n for (const instance of instances) {\n if (!instance.$el.isConnected) {\n instances.delete(instance);\n }\n }\n\n return instances;\n }\n\n constructor(element: HTMLElement) {\n super(element);\n\n this.$on('mounted', () => {\n this.$group.add(this);\n });\n this.$on('destroyed', () => {\n this.$group.delete(this);\n });\n }\n };\n}\n"],
5
- "mappings": "AAgBA,SAAS,SAAqD;AAC5D,SAAQ,WAAW,0BAA0B,oBAAI,IAAoB;AACvE;AAOO,SAAS,UACd,WACA,YAAY,IAC0C;AAEtD,SAAO,MAAM,kBAAmD,UAA8B;AAAA,IAC5F,OAAO,SAAqB;AAAA,MAC1B,GAAG,UAAU;AAAA,MACb,SAAS;AAAA,QACP,OAAO;AAAA,MACT;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,SAAS;AACX,YAAM,QAAQ,GAAG,SAAS,GAAG,KAAK,SAAS,KAAK;AAChD,YAAM,YACJ,OAAa,EAAE,IAAI,KAAK,KAAK,OAAa,EAAE,IAAI,OAAO,oBAAI,IAAI,CAAC,EAAE,IAAI,KAAK;AAE7E,iBAAW,YAAY,WAAW;AAChC,YAAI,CAAC,SAAS,IAAI,aAAa;AAC7B,oBAAU,OAAO,QAAQ;AAAA,QAC3B;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,YAAY,SAAsB;AAChC,YAAM,OAAO;AAEb,WAAK,IAAI,WAAW,MAAM;AACxB,aAAK,OAAO,IAAI,IAAI;AAAA,MACtB,CAAC;AACD,WAAK,IAAI,aAAa,MAAM;AAC1B,aAAK,OAAO,OAAO,IAAI;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { BaseDecorator, BaseInterface } from '../Base/types.js';\nimport type { Base, BaseProps, BaseConfig } from '../Base/index.js';\n\nexport interface WithGroupProps extends BaseProps {\n $options: {\n group: string;\n };\n}\n\nexport interface WithGroupInterface extends BaseInterface {\n get $group(): Set<Base>;\n}\n\n/**\n * Options for the `withGroup` decorator.\n */\nexport interface WithGroupOptions<T extends Base = Base, Scope extends object = object> {\n /**\n * Resolve the scope an instance belongs to. Return `undefined` to fall back\n * to the global (unscoped) registry.\n */\n getScope?: (instance: T) => Scope | undefined;\n /**\n * Resolve the group name of an instance, optionally using its resolved scope.\n * Defaults to `instance.$options.group`.\n */\n getGroup?: (instance: T, scope: Scope | undefined) => string;\n}\n\n/**\n * Get global groups map.\n */\nfunction groups<T extends Base = Base>(): Map<string, Set<T>> {\n return (globalThis.__JS_TOOLKIT_GROUPS__ ??= new Map<string, Set<T>>());\n}\n\n/**\n * Scoped groups registry, keyed by scope identity. Scopes are stored in a\n * `WeakMap` so they can be garbage-collected once no longer referenced.\n */\nconst scopedGroups = new WeakMap<object, Map<string, Set<Base>>>();\n\n/**\n * Enumerate the scoped groups registered for a given scope.\n *\n * Returns the live `Map` of `${namespace}${group}` keys to their members set\n * for the given scope, or an empty `Map` for unknown scopes. Unknown scopes are\n * NOT registered as a side effect of reading.\n */\nexport function getScopedGroups(scope: object): Map<string, Set<Base>> {\n return scopedGroups.get(scope) ?? new Map<string, Set<Base>>();\n}\n\n/**\n * Get the members set for a given `(scope, key)` pair, creating it if needed.\n * When `scope` is `undefined`, the global (unscoped) registry is used.\n */\nfunction resolveGroupSet(scope: object | undefined, key: string): Set<Base> {\n if (scope) {\n let map = scopedGroups.get(scope);\n if (!map) {\n map = new Map<string, Set<Base>>();\n scopedGroups.set(scope, map);\n }\n let set = map.get(key);\n if (!set) {\n set = new Set<Base>();\n map.set(key, set);\n }\n return set;\n }\n\n const registry = groups();\n return registry.get(key) ?? registry.set(key, new Set()).get(key)!;\n}\n\n/**\n * Add group features to easily bind multiple components together.\n *\n * @link https://js-toolkit.studiometa.dev/api/decorators/withGroup.html\n */\nexport function withGroup<S extends Base = Base, Scope extends object = object>(\n BaseClass: typeof Base,\n namespace = '',\n options: WithGroupOptions<S, Scope> = {},\n): BaseDecorator<WithGroupInterface, S, WithGroupProps> {\n const getScope = options.getScope;\n const getGroup = options.getGroup ?? ((instance: S) => instance.$options.group);\n\n // @ts-expect-error Decorators can not be typed.\n return class WithGroup<T extends BaseProps = BaseProps> extends BaseClass<T & WithGroupProps> {\n static config: BaseConfig = {\n ...BaseClass.config,\n options: {\n group: String,\n },\n };\n\n /**\n * The members set remembered at mount time, if any.\n *\n * @private\n */\n __groupSnapshot: Set<Base> | undefined = undefined;\n\n /**\n * Resolve the members set for the current scope and group.\n *\n * @private\n */\n __resolveGroup() {\n const scope = getScope?.(this as unknown as S);\n const group = `${namespace}${getGroup(this as unknown as S, scope)}`;\n return resolveGroupSet(scope, group);\n }\n\n /**\n * Get the group set.\n */\n get $group() {\n const instances = this.__groupSnapshot ?? this.__resolveGroup();\n\n for (const instance of instances) {\n if (!instance.$el.isConnected) {\n instances.delete(instance);\n }\n }\n\n return instances;\n }\n\n constructor(element: HTMLElement) {\n super(element);\n\n this.$on('mounted', () => {\n // Resolve the members set through the public `$group` getter so that\n // subclasses overriding it keep receiving the membership writes. At this\n // point `__groupSnapshot` is still `undefined`, so the base getter\n // resolves fresh (identical behaviour for non-overriding classes).\n // Remember the exact set the instance was added to so that on destroy a\n // member is always removed from the set it joined, even if the scope or\n // group would now resolve differently.\n const instances = this.$group;\n instances.add(this);\n this.__groupSnapshot = instances;\n });\n this.$on('destroyed', () => {\n this.__groupSnapshot?.delete(this);\n this.__groupSnapshot = undefined;\n });\n }\n };\n}\n"],
5
+ "mappings": "AAgCA,SAAS,SAAqD;AAC5D,SAAQ,WAAW,0BAA0B,oBAAI,IAAoB;AACvE;AAMA,MAAM,eAAe,oBAAI,QAAwC;AAS1D,SAAS,gBAAgB,OAAuC;AACrE,SAAO,aAAa,IAAI,KAAK,KAAK,oBAAI,IAAuB;AAC/D;AAMA,SAAS,gBAAgB,OAA2B,KAAwB;AAC1E,MAAI,OAAO;AACT,QAAI,MAAM,aAAa,IAAI,KAAK;AAChC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAuB;AACjC,mBAAa,IAAI,OAAO,GAAG;AAAA,IAC7B;AACA,QAAI,MAAM,IAAI,IAAI,GAAG;AACrB,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAU;AACpB,UAAI,IAAI,KAAK,GAAG;AAAA,IAClB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,OAAO;AACxB,SAAO,SAAS,IAAI,GAAG,KAAK,SAAS,IAAI,KAAK,oBAAI,IAAI,CAAC,EAAE,IAAI,GAAG;AAClE;AAOO,SAAS,UACd,WACA,YAAY,IACZ,UAAsC,CAAC,GACe;AACtD,QAAM,WAAW,QAAQ;AACzB,QAAM,WAAW,QAAQ,aAAa,CAAC,aAAgB,SAAS,SAAS;AAGzE,SAAO,MAAM,kBAAmD,UAA8B;AAAA,IAC5F,OAAO,SAAqB;AAAA,MAC1B,GAAG,UAAU;AAAA,MACb,SAAS;AAAA,QACP,OAAO;AAAA,MACT;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,kBAAyC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOzC,iBAAiB;AACf,YAAM,QAAQ,WAAW,IAAoB;AAC7C,YAAM,QAAQ,GAAG,SAAS,GAAG,SAAS,MAAsB,KAAK,CAAC;AAClE,aAAO,gBAAgB,OAAO,KAAK;AAAA,IACrC;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,SAAS;AACX,YAAM,YAAY,KAAK,mBAAmB,KAAK,eAAe;AAE9D,iBAAW,YAAY,WAAW;AAChC,YAAI,CAAC,SAAS,IAAI,aAAa;AAC7B,oBAAU,OAAO,QAAQ;AAAA,QAC3B;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,YAAY,SAAsB;AAChC,YAAM,OAAO;AAEb,WAAK,IAAI,WAAW,MAAM;AAQxB,cAAM,YAAY,KAAK;AACvB,kBAAU,IAAI,IAAI;AAClB,aAAK,kBAAkB;AAAA,MACzB,CAAC;AACD,WAAK,IAAI,aAAa,MAAM;AAC1B,aAAK,iBAAiB,OAAO,IAAI;AACjC,aAAK,kBAAkB;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -10,4 +10,5 @@ export * from './importWhenPrefersMotion.js';
10
10
  export * from './importWhenVisible.js';
11
11
  export * from './logTree.js';
12
12
  export * from './registerComponent.js';
13
+ export * from './registerComponents.js';
13
14
  export { type QueryOptions, queryComponent, queryComponentAll, closestComponent, } from './queryComponent.js';
package/helpers/index.js CHANGED
@@ -10,6 +10,7 @@ export * from "./importWhenPrefersMotion.js";
10
10
  export * from "./importWhenVisible.js";
11
11
  export * from "./logTree.js";
12
12
  export * from "./registerComponent.js";
13
+ export * from "./registerComponents.js";
13
14
  import {
14
15
  queryComponent,
15
16
  queryComponentAll,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../packages/js-toolkit/helpers/index.ts"],
4
- "sourcesContent": ["export * from './createApp.js';\nexport * from './defineFeatures.js';\nexport * from './getClosestParent.js';\nexport * from './getDirectChildren.js';\nexport * from './getInstanceFromElement.js';\nexport * from './importOnInteraction.js';\nexport * from './importOnMediaQuery.js';\nexport * from './importWhenIdle.js';\nexport * from './importWhenPrefersMotion.js';\nexport * from './importWhenVisible.js';\nexport * from './logTree.js';\nexport * from './registerComponent.js';\nexport {\n type QueryOptions,\n queryComponent,\n queryComponentAll,\n closestComponent,\n} from './queryComponent.js';\n"],
5
- "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
4
+ "sourcesContent": ["export * from './createApp.js';\nexport * from './defineFeatures.js';\nexport * from './getClosestParent.js';\nexport * from './getDirectChildren.js';\nexport * from './getInstanceFromElement.js';\nexport * from './importOnInteraction.js';\nexport * from './importOnMediaQuery.js';\nexport * from './importWhenIdle.js';\nexport * from './importWhenPrefersMotion.js';\nexport * from './importWhenVisible.js';\nexport * from './logTree.js';\nexport * from './registerComponent.js';\nexport * from './registerComponents.js';\nexport {\n type QueryOptions,\n queryComponent,\n queryComponentAll,\n closestComponent,\n} from './queryComponent.js';\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,24 @@
1
1
  import { BaseConstructor } from '../Base/Base.js';
2
2
  /**
3
- * Register globally and mount a given component, or a promise resolving to a component.
3
+ * Register globally and mount a given component.
4
+ *
5
+ * The component can be provided in any of the forms also accepted for child
6
+ * components in `config.components`:
7
+ * - a `Base` constructor;
8
+ * - a promise resolving to a constructor or a module namespace (`import(...)`);
9
+ * - a factory function returning such a promise (`() => import(...)`).
10
+ *
11
+ * Instances are mounted independently: an element that fails to mount is
12
+ * skipped (and logged in development) instead of failing the whole call, so
13
+ * the resolved array contains every instance that mounted successfully.
4
14
  *
5
15
  * @link https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html
6
- * @param ctor The component constructor or a promise resolving to it.
16
+ * @param ctor The component constructor, or a way to resolve it.
7
17
  * @param nameOrSelector The name or selector to used for this component.
8
18
  * @return A promise resolving to an array of mounted component instances.
9
19
  */
10
- export declare function registerComponent<T extends BaseConstructor = BaseConstructor>(ctor: T | Promise<T>, nameOrSelector?: string): Promise<Awaited<InstanceType<T>>[]>;
20
+ export declare function registerComponent<T extends BaseConstructor = BaseConstructor>(ctor: T | Promise<T | {
21
+ default: T;
22
+ }> | (() => Promise<T | {
23
+ default: T;
24
+ }>), nameOrSelector?: string): Promise<Awaited<InstanceType<T>>[]>;
@@ -1,9 +1,28 @@
1
+ import { isDev, isFunction } from "../utils/index.js";
1
2
  async function registerComponent(ctor, nameOrSelector) {
3
+ if (isFunction(ctor) && !("$isBase" in ctor)) {
4
+ ctor = ctor(void 0);
5
+ }
6
+ let Ctor;
2
7
  if (ctor instanceof Promise) {
3
- ctor = await ctor;
8
+ const module = await ctor;
9
+ Ctor = "$isBase" in module ? module : module.default;
10
+ } else {
11
+ Ctor = ctor;
4
12
  }
5
- const instances = await Promise.all(ctor.$register(nameOrSelector));
6
- return instances;
13
+ const results = await Promise.allSettled(Ctor.$register(nameOrSelector));
14
+ return results.flatMap((result) => {
15
+ if (result.status === "fulfilled") {
16
+ return [result.value];
17
+ }
18
+ if (isDev) {
19
+ console.error(
20
+ "[registerComponent] An instance failed to mount and was skipped.",
21
+ result.reason
22
+ );
23
+ }
24
+ return [];
25
+ });
7
26
  }
8
27
  export {
9
28
  registerComponent
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../packages/js-toolkit/helpers/registerComponent.ts"],
4
- "sourcesContent": ["import { BaseConstructor } from '../Base/Base.js';\n\n/**\n * Register globally and mount a given component, or a promise resolving to a component.\n *\n * @link https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html\n * @param ctor The component constructor or a promise resolving to it.\n * @param nameOrSelector The name or selector to used for this component.\n * @return A promise resolving to an array of mounted component instances.\n */\nexport async function registerComponent<T extends BaseConstructor = BaseConstructor>(\n ctor: T | Promise<T>,\n nameOrSelector?: string,\n) {\n if (ctor instanceof Promise) {\n ctor = await ctor;\n }\n\n const instances = await Promise.all(ctor.$register(nameOrSelector));\n return instances;\n}\n"],
5
- "mappings": "AAUA,eAAsB,kBACpB,MACA,gBACA;AACA,MAAI,gBAAgB,SAAS;AAC3B,WAAO,MAAM;AAAA,EACf;AAEA,QAAM,YAAY,MAAM,QAAQ,IAAI,KAAK,UAAU,cAAc,CAAC;AAClE,SAAO;AACT;",
4
+ "sourcesContent": ["import { BaseConstructor, BaseAsyncConstructor } from '../Base/Base.js';\nimport { isDev, isFunction } from '../utils/index.js';\n\n/**\n * Register globally and mount a given component.\n *\n * The component can be provided in any of the forms also accepted for child\n * components in `config.components`:\n * - a `Base` constructor;\n * - a promise resolving to a constructor or a module namespace (`import(...)`);\n * - a factory function returning such a promise (`() => import(...)`).\n *\n * Instances are mounted independently: an element that fails to mount is\n * skipped (and logged in development) instead of failing the whole call, so\n * the resolved array contains every instance that mounted successfully.\n *\n * @link https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html\n * @param ctor The component constructor, or a way to resolve it.\n * @param nameOrSelector The name or selector to used for this component.\n * @return A promise resolving to an array of mounted component instances.\n */\nexport async function registerComponent<T extends BaseConstructor = BaseConstructor>(\n ctor: T | Promise<T | { default: T }> | (() => Promise<T | { default: T }>),\n nameOrSelector?: string,\n) {\n // Resolve factory functions such as `() => import('./Component.js')`, mirroring\n // how lazy child components are declared. A `Base` constructor is a function too,\n // so it is distinguished by its `$isBase` marker.\n if (isFunction(ctor) && !('$isBase' in ctor)) {\n ctor = (ctor as BaseAsyncConstructor)(undefined as never) as Promise<T | { default: T }>;\n }\n\n let Ctor: T;\n if (ctor instanceof Promise) {\n const module = await ctor;\n // A resolved `Base` constructor is used as-is; only a module namespace is\n // unwrapped. Keying on `$isBase` (like the async child resolution) avoids\n // mistaking a user-defined static `default` member for a module namespace.\n Ctor = ('$isBase' in module ? module : (module as { default: T }).default) as T;\n } else {\n Ctor = ctor;\n }\n\n const results = await Promise.allSettled(Ctor.$register(nameOrSelector));\n\n return results.flatMap((result) => {\n if (result.status === 'fulfilled') {\n return [result.value];\n }\n\n if (isDev) {\n console.error(\n '[registerComponent] An instance failed to mount and was skipped.',\n result.reason,\n );\n }\n\n return [];\n });\n}\n"],
5
+ "mappings": "AACA,SAAS,OAAO,kBAAkB;AAoBlC,eAAsB,kBACpB,MACA,gBACA;AAIA,MAAI,WAAW,IAAI,KAAK,EAAE,aAAa,OAAO;AAC5C,WAAQ,KAA8B,MAAkB;AAAA,EAC1D;AAEA,MAAI;AACJ,MAAI,gBAAgB,SAAS;AAC3B,UAAM,SAAS,MAAM;AAIrB,WAAQ,aAAa,SAAS,SAAU,OAA0B;AAAA,EACpE,OAAO;AACL,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,MAAM,QAAQ,WAAW,KAAK,UAAU,cAAc,CAAC;AAEvE,SAAO,QAAQ,QAAQ,CAAC,WAAW;AACjC,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,CAAC,OAAO,KAAK;AAAA,IACtB;AAEA,QAAI,OAAO;AACT,cAAQ;AAAA,QACN;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,CAAC;AAAA,EACV,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,22 @@
1
+ import { BaseConstructor } from '../Base/Base.js';
2
+ /**
3
+ * Register globally and mount multiple components at once.
4
+ *
5
+ * Each component accepts the same forms as {@link registerComponent}: a `Base`
6
+ * constructor, a promise resolving to a constructor or module namespace, or a
7
+ * factory function returning such a promise.
8
+ *
9
+ * Components are registered independently: a single failure (e.g. a rejected
10
+ * dynamic import) does not prevent the others from being registered, and the
11
+ * returned array contains every instance that mounted successfully. Failures
12
+ * are logged in development.
13
+ *
14
+ * @link https://js-toolkit.studiometa.dev/api/helpers/registerComponents.html
15
+ * @param ctors The components, or ways to resolve them.
16
+ * @return A promise resolving to a flat array of mounted component instances.
17
+ */
18
+ export declare function registerComponents<T extends BaseConstructor = BaseConstructor>(...ctors: (T | Promise<T | {
19
+ default: T;
20
+ }> | (() => Promise<T | {
21
+ default: T;
22
+ }>))[]): Promise<Awaited<InstanceType<T>>[]>;
@@ -0,0 +1,21 @@
1
+ import { isDev } from "../utils/index.js";
2
+ import { registerComponent } from "./registerComponent.js";
3
+ async function registerComponents(...ctors) {
4
+ const results = await Promise.allSettled(ctors.map((ctor) => registerComponent(ctor)));
5
+ return results.flatMap((result) => {
6
+ if (result.status === "fulfilled") {
7
+ return result.value;
8
+ }
9
+ if (isDev) {
10
+ console.error(
11
+ "[registerComponents] A component failed to register and was skipped.",
12
+ result.reason
13
+ );
14
+ }
15
+ return [];
16
+ });
17
+ }
18
+ export {
19
+ registerComponents
20
+ };
21
+ //# sourceMappingURL=registerComponents.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../packages/js-toolkit/helpers/registerComponents.ts"],
4
+ "sourcesContent": ["import { BaseConstructor } from '../Base/Base.js';\nimport { isDev } from '../utils/index.js';\nimport { registerComponent } from './registerComponent.js';\n\n/**\n * Register globally and mount multiple components at once.\n *\n * Each component accepts the same forms as {@link registerComponent}: a `Base`\n * constructor, a promise resolving to a constructor or module namespace, or a\n * factory function returning such a promise.\n *\n * Components are registered independently: a single failure (e.g. a rejected\n * dynamic import) does not prevent the others from being registered, and the\n * returned array contains every instance that mounted successfully. Failures\n * are logged in development.\n *\n * @link https://js-toolkit.studiometa.dev/api/helpers/registerComponents.html\n * @param ctors The components, or ways to resolve them.\n * @return A promise resolving to a flat array of mounted component instances.\n */\nexport async function registerComponents<T extends BaseConstructor = BaseConstructor>(\n ...ctors: (T | Promise<T | { default: T }> | (() => Promise<T | { default: T }>))[]\n) {\n const results = await Promise.allSettled(ctors.map((ctor) => registerComponent(ctor)));\n\n return results.flatMap((result) => {\n if (result.status === 'fulfilled') {\n return result.value;\n }\n\n if (isDev) {\n console.error(\n '[registerComponents] A component failed to register and was skipped.',\n result.reason,\n );\n }\n\n return [];\n });\n}\n"],
5
+ "mappings": "AACA,SAAS,aAAa;AACtB,SAAS,yBAAyB;AAkBlC,eAAsB,sBACjB,OACH;AACA,QAAM,UAAU,MAAM,QAAQ,WAAW,MAAM,IAAI,CAAC,SAAS,kBAAkB,IAAI,CAAC,CAAC;AAErF,SAAO,QAAQ,QAAQ,CAAC,WAAW;AACjC,QAAI,OAAO,WAAW,aAAa;AACjC,aAAO,OAAO;AAAA,IAChB;AAEA,QAAI,OAAO;AACT,cAAQ;AAAA,QACN;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO,CAAC;AAAA,EACV,CAAC;AACH;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiometa/js-toolkit",
3
- "version": "3.6.2",
3
+ "version": "3.8.0",
4
4
  "description": "A set of useful little bits of JavaScript to boost your project! 🚀",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fold a value back and forth in the min and max range.
3
+ * @link https://js-toolkit.studiometa.dev/utils/math/fold.html
4
+ */
5
+ export declare function fold(value: number, min: number, max: number): number;
@@ -0,0 +1,14 @@
1
+ import { wrap } from "./wrap.js";
2
+ function fold(value, min, max) {
3
+ const range = max - min;
4
+ if (!Number.isFinite(range) || range <= 0) {
5
+ return min;
6
+ }
7
+ const cycle = range * 2;
8
+ const wrapped = wrap(value, min, min + cycle);
9
+ return wrapped > max ? 2 * max - wrapped : wrapped;
10
+ }
11
+ export {
12
+ fold
13
+ };
14
+ //# sourceMappingURL=fold.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../packages/js-toolkit/utils/math/fold.ts"],
4
+ "sourcesContent": ["import { wrap } from './wrap.js';\n\n/**\n * Fold a value back and forth in the min and max range.\n * @link https://js-toolkit.studiometa.dev/utils/math/fold.html\n */\nexport function fold(value: number, min: number, max: number): number {\n const range = max - min;\n\n if (!Number.isFinite(range) || range <= 0) {\n return min;\n }\n\n const cycle = range * 2;\n const wrapped = wrap(value, min, min + cycle);\n\n return wrapped > max ? 2 * max - wrapped : wrapped;\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAMd,SAAS,KAAK,OAAe,KAAa,KAAqB;AACpE,QAAM,QAAQ,MAAM;AAEpB,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG;AACzC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,QAAQ;AACtB,QAAM,UAAU,KAAK,OAAO,KAAK,MAAM,KAAK;AAE5C,SAAO,UAAU,MAAM,IAAI,MAAM,UAAU;AAC7C;",
6
+ "names": []
7
+ }
@@ -5,6 +5,7 @@ export { createRange } from './createRange.js';
5
5
  export { clamp } from './clamp.js';
6
6
  export { clamp01 } from './clamp01.js';
7
7
  export { damp } from './damp.js';
8
+ export { fold } from './fold.js';
8
9
  export { spring } from './spring.js';
9
10
  export { inertiaFinalValue } from './inertiaFinalValue.js';
10
11
  export { lerp } from './lerp.js';
@@ -5,6 +5,7 @@ import { createRange } from "./createRange.js";
5
5
  import { clamp } from "./clamp.js";
6
6
  import { clamp01 } from "./clamp01.js";
7
7
  import { damp } from "./damp.js";
8
+ import { fold } from "./fold.js";
8
9
  import { spring } from "./spring.js";
9
10
  import { inertiaFinalValue } from "./inertiaFinalValue.js";
10
11
  import { lerp } from "./lerp.js";
@@ -19,6 +20,7 @@ export {
19
20
  createRange,
20
21
  damp,
21
22
  ease,
23
+ fold,
22
24
  inertiaFinalValue,
23
25
  lerp,
24
26
  map,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../packages/js-toolkit/utils/math/index.ts"],
4
- "sourcesContent": ["export * as ease from './ease.js';\nexport * from './createEases.js';\nexport * from './ease.js';\nexport { createRange } from './createRange.js';\nexport { clamp } from './clamp.js';\nexport { clamp01 } from './clamp01.js';\nexport { damp } from './damp.js';\nexport { spring } from './spring.js';\nexport { inertiaFinalValue } from './inertiaFinalValue.js';\nexport { lerp } from './lerp.js';\nexport { map } from './map.js';\nexport { round } from './round.js';\nexport { mean } from './mean.js';\nexport { smoothTo } from './smoothTo.js';\nexport { wrap } from './wrap.js';\n"],
5
- "mappings": "AAAA,YAAY,UAAU;AACtB,cAAc;AACd,cAAc;AACd,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AACtB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,yBAAyB;AAClC,SAAS,YAAY;AACrB,SAAS,WAAW;AACpB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,YAAY;",
4
+ "sourcesContent": ["export * as ease from './ease.js';\nexport * from './createEases.js';\nexport * from './ease.js';\nexport { createRange } from './createRange.js';\nexport { clamp } from './clamp.js';\nexport { clamp01 } from './clamp01.js';\nexport { damp } from './damp.js';\nexport { fold } from './fold.js';\nexport { spring } from './spring.js';\nexport { inertiaFinalValue } from './inertiaFinalValue.js';\nexport { lerp } from './lerp.js';\nexport { map } from './map.js';\nexport { round } from './round.js';\nexport { mean } from './mean.js';\nexport { smoothTo } from './smoothTo.js';\nexport { wrap } from './wrap.js';\n"],
5
+ "mappings": "AAAA,YAAY,UAAU;AACtB,cAAc;AACd,cAAc;AACd,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AACtB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,yBAAyB;AAClC,SAAS,YAAY;AACrB,SAAS,WAAW;AACpB,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,YAAY;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,8 @@
1
1
  function wrap(value, min, max) {
2
2
  const range = max - min;
3
+ if (!Number.isFinite(range)) {
4
+ return value;
5
+ }
3
6
  return min === max ? min : (range + (value - min) % range) % range + min;
4
7
  }
5
8
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../packages/js-toolkit/utils/math/wrap.ts"],
4
- "sourcesContent": ["/**\n * Wrap a value in the min and max range.\n * @link https://js-toolkit.studiometa.dev/utils/math/wrap.html\n */\nexport function wrap(value: number, min: number, max: number): number {\n const range = max - min;\n return min === max ? min : ((range + ((value - min) % range)) % range) + min;\n}\n"],
5
- "mappings": "AAIO,SAAS,KAAK,OAAe,KAAa,KAAqB;AACpE,QAAM,QAAQ,MAAM;AACpB,SAAO,QAAQ,MAAM,OAAQ,SAAU,QAAQ,OAAO,SAAU,QAAS;AAC3E;",
4
+ "sourcesContent": ["/**\n * Wrap a value in the min and max range.\n * @link https://js-toolkit.studiometa.dev/utils/math/wrap.html\n */\nexport function wrap(value: number, min: number, max: number): number {\n const range = max - min;\n\n if (!Number.isFinite(range)) {\n return value;\n }\n\n return min === max ? min : ((range + ((value - min) % range)) % range) + min;\n}\n"],
5
+ "mappings": "AAIO,SAAS,KAAK,OAAe,KAAa,KAAqB;AACpE,QAAM,QAAQ,MAAM;AAEpB,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,MAAM,OAAQ,SAAU,QAAQ,OAAO,SAAU,QAAS;AAC3E;",
6
6
  "names": []
7
7
  }