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.
- package/bin/index.mjs +271 -0
- package/declarations/color-scheme.d.ts +1 -1
- package/declarations/color-scheme.d.ts.map +1 -1
- package/declarations/components/rating/public-types.d.ts +0 -4
- package/declarations/components/rating/public-types.d.ts.map +1 -1
- package/declarations/components/rating/rating.d.ts +9 -1
- package/declarations/components/rating/rating.d.ts.map +1 -1
- package/declarations/components/rating/stars.d.ts.map +1 -1
- package/declarations/components/rating/state.d.ts +4 -0
- package/declarations/components/rating/state.d.ts.map +1 -1
- package/declarations/components/rating/utils.d.ts +0 -1
- package/declarations/components/rating/utils.d.ts.map +1 -1
- package/dist/color-scheme.js +13 -4
- package/dist/color-scheme.js.map +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{rating-CjBVsX6q.js → rating-BrIiwDLw.js} +21 -17
- package/dist/rating-BrIiwDLw.js.map +1 -0
- package/package.json +6 -2
- package/src/-private.ts +4 -0
- package/src/color-scheme.ts +177 -0
- package/src/components/-private/typed-elements.gts +13 -0
- package/src/components/-private/utils.ts +16 -0
- package/src/components/accordion/content.gts +34 -0
- package/src/components/accordion/header.gts +36 -0
- package/src/components/accordion/item.gts +55 -0
- package/src/components/accordion/public.ts +64 -0
- package/src/components/accordion/trigger.gts +32 -0
- package/src/components/accordion.gts +195 -0
- package/src/components/avatar.gts +108 -0
- package/src/components/dialog.gts +234 -0
- package/src/components/external-link.gts +14 -0
- package/src/components/form.gts +75 -0
- package/src/components/heading.gts +36 -0
- package/src/components/keys.gts +53 -0
- package/src/components/layout/hero.css +5 -0
- package/src/components/layout/hero.gts +17 -0
- package/src/components/layout/sticky-footer.css +9 -0
- package/src/components/layout/sticky-footer.gts +40 -0
- package/src/components/link.gts +172 -0
- package/src/components/menu.gts +373 -0
- package/src/components/one-time-password/buttons.gts +31 -0
- package/src/components/one-time-password/input.gts +198 -0
- package/src/components/one-time-password/otp.gts +130 -0
- package/src/components/one-time-password/utils.ts +201 -0
- package/src/components/one-time-password.gts +2 -0
- package/src/components/popover.gts +248 -0
- package/src/components/portal-targets.gts +136 -0
- package/src/components/portal.gts +194 -0
- package/src/components/progress.gts +154 -0
- package/src/components/rating/public-types.ts +44 -0
- package/src/components/rating/range.gts +22 -0
- package/src/components/rating/rating.gts +228 -0
- package/src/components/rating/stars.gts +60 -0
- package/src/components/rating/state.gts +144 -0
- package/src/components/rating/utils.ts +7 -0
- package/src/components/rating.gts +5 -0
- package/src/components/scroller.gts +179 -0
- package/src/components/shadowed.gts +110 -0
- package/src/components/switch.gts +103 -0
- package/src/components/tabs.gts +519 -0
- package/src/components/toggle-group.gts +265 -0
- package/src/components/toggle.gts +81 -0
- package/src/components/violations.css +105 -0
- package/src/components/violations.css.ts +1 -0
- package/src/components/visually-hidden.css +14 -0
- package/src/components/visually-hidden.gts +15 -0
- package/src/components/zoetrope/index.gts +358 -0
- package/src/components/zoetrope/styles.css +40 -0
- package/src/components/zoetrope/types.ts +65 -0
- package/src/components/zoetrope.ts +3 -0
- package/src/dom-context.gts +245 -0
- package/src/floating-ui/component.gts +186 -0
- package/src/floating-ui/middleware.ts +13 -0
- package/src/floating-ui/modifier.ts +183 -0
- package/src/floating-ui.ts +2 -0
- package/src/head.gts +37 -0
- package/src/helpers/body-class.ts +94 -0
- package/src/helpers/link.ts +125 -0
- package/src/helpers/service.ts +25 -0
- package/src/helpers.ts +2 -0
- package/src/iframe.ts +31 -0
- package/src/index.ts +43 -0
- package/src/load.gts +77 -0
- package/src/narrowing.ts +7 -0
- package/src/on-resize.ts +64 -0
- package/src/proper-links.ts +140 -0
- package/src/qp.ts +107 -0
- package/src/resize-observer.ts +132 -0
- package/src/service.ts +103 -0
- package/src/store.ts +72 -0
- package/src/styles.css.ts +5 -0
- package/src/tabster.ts +54 -0
- package/src/template-registry.ts +44 -0
- package/src/test-support/a11y.ts +50 -0
- package/src/test-support/dom.ts +112 -0
- package/src/test-support/otp.ts +64 -0
- package/src/test-support/rating.ts +144 -0
- package/src/test-support/routing.ts +62 -0
- package/src/test-support/zoetrope.ts +51 -0
- package/src/test-support.gts +6 -0
- package/src/type-utils.ts +1 -0
- package/src/utils.ts +75 -0
- package/src/viewport/in-viewport.gts +128 -0
- package/src/viewport/viewport.ts +122 -0
- package/src/viewport.ts +2 -0
- 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
|
+
}
|
package/src/narrowing.ts
ADDED
package/src/on-resize.ts
ADDED
|
@@ -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
|
+
}
|