ember-primitives 0.49.0 → 0.50.1

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.
Files changed (107) hide show
  1. package/bin/index.mjs +271 -0
  2. package/declarations/color-scheme.d.ts +1 -1
  3. package/declarations/color-scheme.d.ts.map +1 -1
  4. package/declarations/components/rating/public-types.d.ts +0 -4
  5. package/declarations/components/rating/public-types.d.ts.map +1 -1
  6. package/declarations/components/rating/rating.d.ts +9 -1
  7. package/declarations/components/rating/rating.d.ts.map +1 -1
  8. package/declarations/components/rating/stars.d.ts.map +1 -1
  9. package/declarations/components/rating/state.d.ts +4 -0
  10. package/declarations/components/rating/state.d.ts.map +1 -1
  11. package/declarations/components/rating/utils.d.ts +0 -1
  12. package/declarations/components/rating/utils.d.ts.map +1 -1
  13. package/dist/color-scheme.js +13 -4
  14. package/dist/color-scheme.js.map +1 -1
  15. package/dist/components/rating.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/{rating-CjBVsX6q.js → rating-BrIiwDLw.js} +21 -17
  18. package/dist/rating-BrIiwDLw.js.map +1 -0
  19. package/package.json +6 -2
  20. package/src/-private.ts +4 -0
  21. package/src/color-scheme.ts +177 -0
  22. package/src/components/-private/typed-elements.gts +13 -0
  23. package/src/components/-private/utils.ts +16 -0
  24. package/src/components/accordion/content.gts +34 -0
  25. package/src/components/accordion/header.gts +36 -0
  26. package/src/components/accordion/item.gts +55 -0
  27. package/src/components/accordion/public.ts +64 -0
  28. package/src/components/accordion/trigger.gts +32 -0
  29. package/src/components/accordion.gts +195 -0
  30. package/src/components/avatar.gts +108 -0
  31. package/src/components/dialog.gts +234 -0
  32. package/src/components/external-link.gts +14 -0
  33. package/src/components/form.gts +75 -0
  34. package/src/components/heading.gts +36 -0
  35. package/src/components/keys.gts +53 -0
  36. package/src/components/layout/hero.css +5 -0
  37. package/src/components/layout/hero.gts +17 -0
  38. package/src/components/layout/sticky-footer.css +9 -0
  39. package/src/components/layout/sticky-footer.gts +40 -0
  40. package/src/components/link.gts +172 -0
  41. package/src/components/menu.gts +373 -0
  42. package/src/components/one-time-password/buttons.gts +31 -0
  43. package/src/components/one-time-password/input.gts +198 -0
  44. package/src/components/one-time-password/otp.gts +130 -0
  45. package/src/components/one-time-password/utils.ts +201 -0
  46. package/src/components/one-time-password.gts +2 -0
  47. package/src/components/popover.gts +248 -0
  48. package/src/components/portal-targets.gts +136 -0
  49. package/src/components/portal.gts +194 -0
  50. package/src/components/progress.gts +154 -0
  51. package/src/components/rating/public-types.ts +44 -0
  52. package/src/components/rating/range.gts +22 -0
  53. package/src/components/rating/rating.gts +228 -0
  54. package/src/components/rating/stars.gts +60 -0
  55. package/src/components/rating/state.gts +144 -0
  56. package/src/components/rating/utils.ts +7 -0
  57. package/src/components/rating.gts +5 -0
  58. package/src/components/scroller.gts +179 -0
  59. package/src/components/shadowed.gts +110 -0
  60. package/src/components/switch.gts +103 -0
  61. package/src/components/tabs.gts +519 -0
  62. package/src/components/toggle-group.gts +265 -0
  63. package/src/components/toggle.gts +81 -0
  64. package/src/components/violations.css +105 -0
  65. package/src/components/violations.css.ts +1 -0
  66. package/src/components/visually-hidden.css +14 -0
  67. package/src/components/visually-hidden.gts +15 -0
  68. package/src/components/zoetrope/index.gts +358 -0
  69. package/src/components/zoetrope/styles.css +40 -0
  70. package/src/components/zoetrope/types.ts +65 -0
  71. package/src/components/zoetrope.ts +3 -0
  72. package/src/dom-context.gts +245 -0
  73. package/src/floating-ui/component.gts +186 -0
  74. package/src/floating-ui/middleware.ts +13 -0
  75. package/src/floating-ui/modifier.ts +183 -0
  76. package/src/floating-ui.ts +2 -0
  77. package/src/head.gts +37 -0
  78. package/src/helpers/body-class.ts +94 -0
  79. package/src/helpers/link.ts +125 -0
  80. package/src/helpers/service.ts +25 -0
  81. package/src/helpers.ts +2 -0
  82. package/src/iframe.ts +31 -0
  83. package/src/index.ts +43 -0
  84. package/src/load.gts +77 -0
  85. package/src/narrowing.ts +7 -0
  86. package/src/on-resize.ts +64 -0
  87. package/src/proper-links.ts +140 -0
  88. package/src/qp.ts +107 -0
  89. package/src/resize-observer.ts +132 -0
  90. package/src/service.ts +103 -0
  91. package/src/store.ts +72 -0
  92. package/src/styles.css.ts +5 -0
  93. package/src/tabster.ts +54 -0
  94. package/src/template-registry.ts +44 -0
  95. package/src/test-support/a11y.ts +50 -0
  96. package/src/test-support/dom.ts +112 -0
  97. package/src/test-support/otp.ts +64 -0
  98. package/src/test-support/rating.ts +144 -0
  99. package/src/test-support/routing.ts +62 -0
  100. package/src/test-support/zoetrope.ts +51 -0
  101. package/src/test-support.gts +6 -0
  102. package/src/type-utils.ts +1 -0
  103. package/src/utils.ts +75 -0
  104. package/src/viewport/in-viewport.gts +128 -0
  105. package/src/viewport/viewport.ts +122 -0
  106. package/src/viewport.ts +2 -0
  107. package/dist/rating-CjBVsX6q.js.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * DANGER: this is a *barrel file*
3
+ *
4
+ * It forces the whole library to be loaded and all dependencies.
5
+ *
6
+ * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.
7
+ */
8
+ import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';
9
+
10
+ if (macroCondition(isDevelopingApp())) {
11
+ importSync('./components/violations.css');
12
+ }
13
+
14
+ export { Accordion } from './components/accordion.gts';
15
+ export type {
16
+ AccordionContentExternalSignature,
17
+ AccordionHeaderExternalSignature,
18
+ AccordionItemExternalSignature,
19
+ AccordionTriggerExternalSignature,
20
+ } from './components/accordion/public.ts';
21
+ export { Avatar } from './components/avatar.gts';
22
+ export { Dialog, Dialog as Modal } from './components/dialog.gts';
23
+ export { ExternalLink } from './components/external-link.gts';
24
+ export { Form } from './components/form.gts';
25
+ export { Key, KeyCombo } from './components/keys.gts';
26
+ export { StickyFooter } from './components/layout/sticky-footer.gts';
27
+ export { Link } from './components/link.gts';
28
+ export { Menu } from './components/menu.gts';
29
+ export { OTP, OTPInput } from './components/one-time-password.gts';
30
+ export { Popover } from './components/popover.gts';
31
+ export { Portal } from './components/portal.gts';
32
+ export { PortalTargets } from './components/portal-targets.gts';
33
+ export { TARGETS as PORTALS } from './components/portal-targets.gts';
34
+ export { Progress } from './components/progress.gts';
35
+ export { Rating } from './components/rating.gts';
36
+ export { Scroller } from './components/scroller.gts';
37
+ export { Shadowed } from './components/shadowed.gts';
38
+ export { Switch } from './components/switch.gts';
39
+ export { Toggle } from './components/toggle.gts';
40
+ export { ToggleGroup } from './components/toggle-group.gts';
41
+ export { VisuallyHidden } from './components/visually-hidden.gts';
42
+ export { Zoetrope } from './components/zoetrope.ts';
43
+ export * from './helpers.ts';
package/src/load.gts ADDED
@@ -0,0 +1,77 @@
1
+ import { setComponentTemplate } from "@ember/component";
2
+ import templateOnly from "@ember/component/template-only";
3
+ // Have to use these until min ember version is like 6.3 or something
4
+ import { precompileTemplate } from "@ember/template-compilation";
5
+
6
+ import { getPromiseState } from "reactiveweb/get-promise-state";
7
+
8
+ import type { ComponentLike } from "@glint/template";
9
+
10
+ interface LoadSignature<
11
+ Expected = {
12
+ Args: any;
13
+ },
14
+ > {
15
+ Blocks: {
16
+ loading: [];
17
+ error: [
18
+ {
19
+ original: unknown;
20
+ reason: string;
21
+ },
22
+ ];
23
+ success?: [component: ComponentLike<Expected>];
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Loads a value / promise / function providing state for the lifetime of that value / promise / function.
29
+ *
30
+ * Can be used for manual bundle splitting via await importing components.
31
+ *
32
+ * @example
33
+ * ```gjs
34
+ * import { load } from 'ember-primitives/load';
35
+ *
36
+ * const Loader = load(() => import('./routes/sub-route.gts'));
37
+ *
38
+ * <template>
39
+ * <Loader>
40
+ * <:loading> ... loading ... </:loading>
41
+ * <:error as |error|> ... error! {{error.reason}} </:error>
42
+ * <:success as |component|> <component /> </:success>
43
+ * </Loader>
44
+ * </template>
45
+ * ```
46
+ */
47
+ export function load<ExpectedSignature, Value>(
48
+ fn: Value | Promise<Value> | (() => Promise<Value>) | (() => Value),
49
+ ): ComponentLike<LoadSignature<ExpectedSignature>> {
50
+ return setComponentTemplate(
51
+ precompileTemplate(
52
+ `{{#let (getPromiseState fn) as |state|}}
53
+ {{#if state.isLoading}}
54
+ {{yield to="loading"}}
55
+ {{else if state.error}}
56
+ {{yield state.error to="error"}}
57
+ {{else if state.resolved}}
58
+ {{#if (has-block "success")}}
59
+ {{yield state.resolved to="success"}}
60
+ {{else}}
61
+ <state.component />
62
+ {{/if}}
63
+ {{/if}}
64
+ {{/let}}`,
65
+ {
66
+ strictMode: true,
67
+ /**
68
+ * The old setComponentTemplate + precompileTemplate combo
69
+ * does not allow defining things in this scope object,
70
+ * we _have_ to use the shorthand.
71
+ */
72
+ scope: () => ({ fn, getPromiseState }),
73
+ },
74
+ ),
75
+ templateOnly(),
76
+ ) as ComponentLike<LoadSignature<ExpectedSignature>>;
77
+ }
@@ -0,0 +1,7 @@
1
+ export function isString(x: unknown): x is string {
2
+ return typeof x === 'string';
3
+ }
4
+
5
+ export function isElement(x: unknown): x is Element {
6
+ return x instanceof Element;
7
+ }
@@ -0,0 +1,64 @@
1
+ import { assert } from '@ember/debug';
2
+ import { registerDestructor } from '@ember/destroyable';
3
+
4
+ import Modifier, { type ArgsFor } from 'ember-modifier';
5
+
6
+ import { resizeObserver } from './resize-observer.ts';
7
+
8
+ import type Owner from '@ember/owner';
9
+
10
+ // re-export provided for convenience
11
+ export { ignoreROError } from './resize-observer.ts';
12
+
13
+ export interface Signature {
14
+ /**
15
+ * Any element that is resizable can have onResize attached
16
+ */
17
+ Element: Element;
18
+ Args: {
19
+ Positional: [
20
+ /**
21
+ * The ResizeObserver callback will only receive
22
+ * one entry per resize event.
23
+ *
24
+ * See: [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
25
+ */
26
+ callback: (entry: ResizeObserverEntry) => void,
27
+ ];
28
+ };
29
+ }
30
+
31
+ class OnResize extends Modifier<Signature> {
32
+ #callback: ((entry: ResizeObserverEntry) => void) | null = null;
33
+ #element: Element | null = null;
34
+
35
+ #resizeObserver = resizeObserver(this);
36
+
37
+ constructor(owner: Owner, args: ArgsFor<Signature>) {
38
+ super(owner, args);
39
+
40
+ registerDestructor(this, () => {
41
+ if (this.#element && this.#callback) {
42
+ this.#resizeObserver.unobserve(this.#element, this.#callback);
43
+ }
44
+ });
45
+ }
46
+
47
+ modify(element: Element, [callback]: [callback: (entry: ResizeObserverEntry) => void]) {
48
+ assert(
49
+ `{{onResize}}: callback must be a function, but was ${callback as unknown as string}`,
50
+ typeof callback === 'function'
51
+ );
52
+
53
+ if (this.#element && this.#callback) {
54
+ this.#resizeObserver.unobserve(this.#element, this.#callback);
55
+ }
56
+
57
+ this.#resizeObserver.observe(element, callback);
58
+
59
+ this.#callback = callback;
60
+ this.#element = element;
61
+ }
62
+ }
63
+
64
+ export const onResize = OnResize;
@@ -0,0 +1,140 @@
1
+ import { assert } from '@ember/debug';
2
+ import { registerDestructor } from '@ember/destroyable';
3
+ import { getOwner } from '@ember/owner';
4
+
5
+ import { getAnchor, shouldHandle } from 'should-handle-link';
6
+
7
+ import type { Newable } from './type-utils.ts';
8
+ import type EmberRouter from '@ember/routing/router';
9
+ import type RouterService from '@ember/routing/router-service';
10
+
11
+ export { shouldHandle } from 'should-handle-link';
12
+
13
+ export interface Options {
14
+ ignore?: string[];
15
+ }
16
+
17
+ export function properLinks(
18
+ options: Options
19
+ ): <Instance extends object, Klass = { new (...args: any[]): Instance }>(klass: Klass) => Klass;
20
+
21
+ export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
22
+ klass: Klass
23
+ ): Klass;
24
+ /**
25
+ * @internal
26
+ */
27
+ export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
28
+ options: Options,
29
+ klass: Klass
30
+ ): Klass;
31
+
32
+ export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
33
+ ...args: [Options] | [Klass] | [Options, Klass]
34
+ ): Klass | ((klass: Klass) => Klass) {
35
+ let options: Options = {};
36
+
37
+ let klass: undefined | Klass = undefined;
38
+
39
+ if (args.length === 2) {
40
+ options = args[0];
41
+ klass = args[1];
42
+ } else if (args.length === 1) {
43
+ if (typeof args[0] === 'object') {
44
+ // TODO: how to get first arg type correct?
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
46
+ return (klass: Klass) => properLinks(args[0] as any, klass);
47
+ } else {
48
+ klass = args[0];
49
+ }
50
+ }
51
+
52
+ const ignore = options.ignore || [];
53
+
54
+ assert(`klass was not defined. possibile incorrect arity given to properLinks`, klass);
55
+
56
+ return class RouterWithProperLinks extends (klass as unknown as Newable<EmberRouter>) {
57
+ // SAFETY: we literally do not care about the args' type here,
58
+ // because we just call super
59
+ constructor(...args: any[]) {
60
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
61
+ super(...args);
62
+
63
+ setup(this, ignore);
64
+ }
65
+ } as unknown as Klass;
66
+ }
67
+
68
+ /**
69
+ * Setup proper links without a decorator.
70
+ * This function only requires that a framework object with an owner is passed.
71
+ */
72
+ export function setup(parent: object, ignore?: string[]) {
73
+ const handler = (event: MouseEvent) => {
74
+ /**
75
+ * event.target may not be an anchor,
76
+ * it may be a span, svg, img, or any number of elements nested in <a>...</a>
77
+ */
78
+ const interactive = getAnchor(event);
79
+
80
+ if (!interactive) return;
81
+
82
+ const owner = getOwner(parent);
83
+
84
+ assert('owner is not present', owner);
85
+
86
+ const routerService = owner.lookup('service:router');
87
+
88
+ handle(routerService, interactive, ignore ?? [], event);
89
+ };
90
+
91
+ document.body.addEventListener('click', handler, false);
92
+
93
+ registerDestructor(parent, () => document.body.removeEventListener('click', handler));
94
+ }
95
+
96
+ export function handle(
97
+ router: RouterService,
98
+ element: HTMLAnchorElement,
99
+ ignore: string[],
100
+ event: MouseEvent
101
+ ) {
102
+ if (!shouldHandle(location.href, element, event, ignore)) {
103
+ return;
104
+ }
105
+
106
+ const url = new URL(element.href);
107
+
108
+ const fullHref = `${url.pathname}${url.search}${url.hash}`;
109
+
110
+ const rootURL = router.rootURL;
111
+
112
+ let withoutRootURL = fullHref.slice(rootURL.length);
113
+
114
+ // re-add the "root" sigil
115
+ // we removed it when we chopped off the rootURL,
116
+ // because the rootURL often has this attached to it as well
117
+ if (!withoutRootURL.startsWith('/')) {
118
+ withoutRootURL = `/${withoutRootURL}`;
119
+ }
120
+
121
+ try {
122
+ const routeInfo = router.recognize(fullHref);
123
+
124
+ if (routeInfo) {
125
+ event.preventDefault();
126
+ event.stopImmediatePropagation();
127
+ event.stopPropagation();
128
+
129
+ router.transitionTo(withoutRootURL);
130
+
131
+ return false;
132
+ }
133
+ } catch (e) {
134
+ if (e instanceof Error && e.name === 'UnrecognizedURLError') {
135
+ return;
136
+ }
137
+
138
+ throw e;
139
+ }
140
+ }
package/src/qp.ts ADDED
@@ -0,0 +1,107 @@
1
+ import Helper from '@ember/component/helper';
2
+ import { assert } from '@ember/debug';
3
+ import { service } from '@ember/service';
4
+
5
+ import type RouterService from '@ember/routing/router-service';
6
+
7
+ interface Signature {
8
+ Args: {
9
+ Positional: [string];
10
+ };
11
+ Return: string | undefined;
12
+ }
13
+
14
+ /**
15
+ * Grabs a query-param off the current route from the router service.
16
+ *
17
+ * ```gjs
18
+ * import { qp } from 'ember-primitives/qp';
19
+ *
20
+ * <template>
21
+ * {{qp "query-param"}}
22
+ * </template>
23
+ * ```
24
+ */
25
+ export class qp extends Helper<Signature> {
26
+ @service declare router: RouterService;
27
+
28
+ compute([name]: [string]): string | undefined {
29
+ assert('A queryParam name is required', name);
30
+
31
+ return this.router.currentRoute?.queryParams?.[name] as string | undefined;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Returns a string for use as an `href` on `<a>` tags, updated with the passed query param
37
+ *
38
+ * ```gjs
39
+ * import { withQP } from 'ember-primitives/qp';
40
+ *
41
+ * <template>
42
+ * <a href={{withQP "foo" "2"}}>
43
+ * ...
44
+ * </a>
45
+ * </template>
46
+ * ```
47
+ */
48
+ export class withQP extends Helper<{ Args: { Positional: [string, string] }; Return: string }> {
49
+ @service declare router: RouterService;
50
+
51
+ compute([qpName, nextValue]: [string, string]) {
52
+ const existing = this.router.currentURL;
53
+
54
+ assert('A queryParam name is required', qpName);
55
+ assert('There is no currentURL', existing);
56
+
57
+ const url = new URL(existing, location.origin);
58
+
59
+ url.searchParams.set(qpName, nextValue);
60
+
61
+ return url.href;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Cast a query-param string value to a boolean
67
+ *
68
+ * ```gjs
69
+ * import { castToBoolean, qp } from 'ember-primitives/qp';
70
+ *
71
+ * <template>
72
+ * {{#if (castToBoolean (qp 'the-qp'))}}
73
+ * ...
74
+ * {{/if}}
75
+ * </template>
76
+ * ```
77
+ *
78
+ * The following values are considered "false"
79
+ * - undefined
80
+ * - ""
81
+ * - "0"
82
+ * - false
83
+ * - "f"
84
+ * - "off"
85
+ * - "no"
86
+ * - "null"
87
+ * - "undefined"
88
+ *
89
+ * All other values are considered truthy
90
+ */
91
+ export function castToBoolean(x: string | undefined) {
92
+ if (!x) return false;
93
+
94
+ const isFalsey =
95
+ x === '0' ||
96
+ x === 'false' ||
97
+ x === 'f' ||
98
+ x === 'null' ||
99
+ x === 'off' ||
100
+ x === 'undefined' ||
101
+ x === 'no';
102
+
103
+ if (isFalsey) return false;
104
+
105
+ // All other values are considered truthy
106
+ return true;
107
+ }
@@ -0,0 +1,132 @@
1
+ import { assert } from '@ember/debug';
2
+ import { registerDestructor } from '@ember/destroyable';
3
+
4
+ import { createStore } from './store.ts';
5
+ import { findOwner } from './utils.ts';
6
+
7
+ /**
8
+ * Creates or returns the ResizeObserverManager.
9
+ *
10
+ * Only one of these will exist per owner.
11
+ *
12
+ * Has only two methods:
13
+ * - observe(element, callback: (resizeObserverEntry) => void)
14
+ * - unobserve(element, callback: (resizeObserverEntry) => void)
15
+ *
16
+ * Like with the underlying ResizeObserver API (and all event listeners),
17
+ * the callback passed to unobserved must be the same reference as the one
18
+ * passed to observe.
19
+ */
20
+ export function resizeObserver(context: object) {
21
+ const owner = findOwner(context);
22
+
23
+ assert(
24
+ `Could not find owner on the passed context (to resizeObserver). resizeObserver can only be used on an object whos lifetime is in someone entangled with the application (which incidentally has an "owner").`,
25
+ owner
26
+ );
27
+
28
+ return createStore(owner, ResizeObserverManager);
29
+ }
30
+
31
+ class ResizeObserverManager {
32
+ #callbacks = new WeakMap<Element, Set<(entry: ResizeObserverEntry) => unknown>>();
33
+
34
+ #handleResize = (entries: ResizeObserverEntry[]) => {
35
+ for (const entry of entries) {
36
+ const callbacks = this.#callbacks.get(entry.target);
37
+
38
+ if (callbacks) {
39
+ for (const callback of callbacks) {
40
+ callback(entry);
41
+ }
42
+ }
43
+ }
44
+ };
45
+ #observer = new ResizeObserver(this.#handleResize);
46
+
47
+ constructor() {
48
+ ignoreROError();
49
+
50
+ registerDestructor(this, () => {
51
+ this.#observer?.disconnect();
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Initiate the observing of the `element` or add an additional `callback`
57
+ * if the `element` is already observed.
58
+ *
59
+ * @param {object} element
60
+ * @param {function} callback The `callback` is called whenever the size of
61
+ * the `element` changes. It is called with `ResizeObserverEntry` object
62
+ * for the particular `element`.
63
+ */
64
+ observe(element: Element, callback: (entry: ResizeObserverEntry) => unknown) {
65
+ const callbacks = this.#callbacks.get(element);
66
+
67
+ if (callbacks) {
68
+ callbacks.add(callback);
69
+ } else {
70
+ this.#callbacks.set(element, new Set([callback]));
71
+ this.#observer.observe(element);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * End the observing of the `element` or just remove the provided `callback`.
77
+ *
78
+ * It will unobserve the `element` if the `callback` is not provided
79
+ * or there are no more callbacks left for this `element`.
80
+ *
81
+ * @param {object} element
82
+ * @param {function?} callback - The `callback` to remove from the listeners
83
+ * of the `element` size changes.
84
+ */
85
+ unobserve(element: Element, callback: (entry: ResizeObserverEntry) => unknown) {
86
+ const callbacks = this.#callbacks.get(element);
87
+
88
+ if (!callbacks) {
89
+ return;
90
+ }
91
+
92
+ callbacks.delete(callback);
93
+
94
+ if (!callback || !callbacks.size) {
95
+ this.#callbacks.delete(element);
96
+ this.#observer.unobserve(element);
97
+ }
98
+ }
99
+ }
100
+
101
+ const errorMessages = [
102
+ 'ResizeObserver loop limit exceeded',
103
+ 'ResizeObserver loop completed with undelivered notifications.',
104
+ ];
105
+
106
+ /**
107
+ * Ignores "ResizeObserver loop limit exceeded" error in Ember tests.
108
+ *
109
+ * This "error" is safe to ignore as it is just a warning message,
110
+ * telling that the "looping" observation will be skipped in the current frame,
111
+ * and will be delivered in the next one.
112
+ *
113
+ * For some reason, it is fired as an `error` event at `window` failing Ember
114
+ * tests and exploding Sentry with errors that must be ignored.
115
+ */
116
+ export function ignoreROError() {
117
+ if (typeof window.onerror !== 'function') {
118
+ return;
119
+ }
120
+
121
+ const onError = window.onerror;
122
+
123
+ window.onerror = (...args) => {
124
+ const [message] = args;
125
+
126
+ if (typeof message === 'string') {
127
+ if (errorMessages.includes(message)) return true;
128
+ }
129
+
130
+ onError(...args);
131
+ };
132
+ }
package/src/service.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { assert } from '@ember/debug';
2
+
3
+ import { getPromiseState } from 'reactiveweb/get-promise-state';
4
+
5
+ import { createStore } from './store.ts';
6
+ import { findOwner } from './utils.ts';
7
+
8
+ import type { Newable } from './type-utils.ts';
9
+
10
+ /*
11
+ import type { Newable } from './type-utils.ts';
12
+ import type { Registry } from '@ember/service';
13
+ import type Service from '@ember/service';
14
+
15
+ type Decorator = ReturnType<typeof emberService>;
16
+
17
+ // export function service<Key extends keyof Registry>(
18
+ // context: object,
19
+ // serviceName: Key
20
+ // ): Registry[Key] & Service;
21
+ export function service<Class extends object>(
22
+ context: object,
23
+ serviceDefinition: Newable<Class>
24
+ ): Class;
25
+ export function service<Class extends object>(serviceDefinition: Newable<Class>): Decorator;
26
+ export function service<Key extends keyof Registry>(serviceName: Key): Decorator;
27
+ export function service(prototype: object, name: string | symbol, descriptor: unknown): void;
28
+ export function service<Value, Result>(
29
+ context: object,
30
+ fn: Parameters<typeof getPromiseState<Value, Result>>[0]
31
+ ): ReturnType<typeof getPromiseState<Value, Result>>;
32
+ export function service<Value, Result>(
33
+ fn: Parameters<typeof getPromiseState<Value, Result>>[0]
34
+ ): Decorator;
35
+ */
36
+
37
+ /**
38
+ * Instantiates a class once per application instance.
39
+ *
40
+ *
41
+ */
42
+ export function createService<Instance extends object>(
43
+ context: object,
44
+ theClass: Newable<Instance> | (() => Instance)
45
+ ): Instance {
46
+ const owner = findOwner(context);
47
+
48
+ assert(
49
+ `Could not find owner / application instance. Cannot create a instance tied to the application lifetime without the application`,
50
+ owner
51
+ );
52
+
53
+ return createStore(owner, theClass);
54
+ }
55
+
56
+ const promiseCache = new WeakMap<() => any, unknown>();
57
+
58
+ /**
59
+ * Lazily instantiate a service.
60
+ *
61
+ * This is a replacement / alternative API for ember's `@service` decorator from `@ember/service`.
62
+ *
63
+ * For example
64
+ * ```js
65
+ * import { service } from 'ember-primitives/service';
66
+ *
67
+ * const loader = () => {
68
+ * let module = await import('./foo/file/with/class.js');
69
+ * return () => new module.MyState();
70
+ * }
71
+ *
72
+ * class Demo extends Component {
73
+ * state = createAsyncService(this, loader);
74
+ * }
75
+ * ```
76
+ *
77
+ * The important thing is for repeat usage of `createAsyncService` the second parameter,
78
+ * (loader in this case), must be shared between all usages.
79
+ *
80
+ * This is an alternative to using `createStore` inside an await'd component,
81
+ * or a component rendered with [`getPromiseState`](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html)
82
+ * ```
83
+ */
84
+ export function createAsyncService<Instance extends object>(
85
+ context: object,
86
+ theClass: () => Promise<Newable<Instance> | (() => Instance)>
87
+ ): ReturnType<typeof getPromiseState<unknown, Instance>> {
88
+ let existing = promiseCache.get(theClass);
89
+
90
+ if (!existing) {
91
+ existing = async () => {
92
+ const result = await theClass();
93
+
94
+ // Pay no attention to the lies, I don't know what the right type is here
95
+ return createStore(context, result as Newable<Instance>);
96
+ };
97
+
98
+ promiseCache.set(theClass, existing);
99
+ }
100
+
101
+ // Pay no attention to the TS inference crime here
102
+ return getPromiseState<unknown, Instance>(existing);
103
+ }