@adobe-commerce/elsie 1.4.0-alpha1 → 1.4.0-alpha3
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/builders/gql/index.js +27 -1
- package/bin/builders/gql/validate.js +135 -0
- package/package.json +3 -1
- package/src/components/Button/Button.css +9 -9
- package/src/components/Icon/Icon.tsx +19 -7
- package/src/components/Modal/Modal.stories.tsx +62 -0
- package/src/components/Portal/Portal.tsx +15 -9
- package/src/components/Price/Price.tsx +27 -7
- package/src/docs/API/initializer.mdx +69 -7
- package/src/lib/aem/assets.ts +218 -102
- package/src/lib/index.ts +1 -0
- package/src/lib/initializer.ts +18 -5
- package/src/lib/locale-config.ts +34 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const createOrClearDirectory = require('./createOrClearDirectory');
|
|
3
3
|
const getSchemaRef = require('./getSchemaRef');
|
|
4
|
+
const validate = require('./validate');
|
|
4
5
|
require('dotenv').config();
|
|
5
6
|
|
|
6
7
|
const generate = require('@graphql-codegen/cli').generate;
|
|
@@ -67,5 +68,30 @@ module.exports = async function generateResourceBuilder(yargs) {
|
|
|
67
68
|
},
|
|
68
69
|
});
|
|
69
70
|
})
|
|
70
|
-
.
|
|
71
|
+
.command(
|
|
72
|
+
'validate',
|
|
73
|
+
'Validate GraphQL operations',
|
|
74
|
+
async (yargs) => {
|
|
75
|
+
return yargs
|
|
76
|
+
.option('source', {
|
|
77
|
+
alias: 's',
|
|
78
|
+
describe: 'Path to the source code containing GraphQL operations',
|
|
79
|
+
type: 'array',
|
|
80
|
+
string: true,
|
|
81
|
+
demandOption: true,
|
|
82
|
+
})
|
|
83
|
+
.option('endpoints', {
|
|
84
|
+
alias: 'e',
|
|
85
|
+
describe: 'Path to GraphQL endpoints',
|
|
86
|
+
type: 'array',
|
|
87
|
+
string: true,
|
|
88
|
+
demandOption: true,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
async (argv) => {
|
|
92
|
+
const { source, endpoints } = argv;
|
|
93
|
+
await validate(source, endpoints);
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
.demandCommand(1, 1, 'choose a command: types, mocks or validate');
|
|
71
97
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fsPromises = require('node:fs/promises');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const parser = require('@babel/parser');
|
|
5
|
+
const traverse = require('@babel/traverse');
|
|
6
|
+
const { getIntrospectionQuery, buildClientSchema, parse, validate } = require('graphql');
|
|
7
|
+
|
|
8
|
+
async function walk(dir, collected = []) {
|
|
9
|
+
const dirents = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
for (const d of dirents) {
|
|
12
|
+
const full = path.resolve(dir, d.name);
|
|
13
|
+
|
|
14
|
+
if (d.isDirectory()) {
|
|
15
|
+
// skip node_modules and “hidden” folders such as .git
|
|
16
|
+
if (d.name === 'node_modules' || d.name.startsWith('.')) continue;
|
|
17
|
+
await walk(full, collected);
|
|
18
|
+
} else if (/\.(c?m?js|ts|tsx)$/.test(d.name)) {
|
|
19
|
+
collected.push(full);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return collected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractConstants(code) {
|
|
26
|
+
const ast = parser.parse(code, {
|
|
27
|
+
sourceType: 'unambiguous',
|
|
28
|
+
plugins: [
|
|
29
|
+
'typescript',
|
|
30
|
+
'jsx',
|
|
31
|
+
'classProperties',
|
|
32
|
+
'objectRestSpread',
|
|
33
|
+
'dynamicImport',
|
|
34
|
+
'optionalChaining',
|
|
35
|
+
'nullishCoalescingOperator',
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
const found = [];
|
|
39
|
+
traverse.default(ast, {
|
|
40
|
+
VariableDeclaration(path) {
|
|
41
|
+
if (path.node.kind !== 'const') return;
|
|
42
|
+
for (const decl of path.node.declarations) {
|
|
43
|
+
const { id, init } = decl;
|
|
44
|
+
if (!init || id.type !== 'Identifier') continue;
|
|
45
|
+
let text = null;
|
|
46
|
+
switch (init.type) {
|
|
47
|
+
case 'TemplateLiteral': {
|
|
48
|
+
// join all raw chunks; ignores embedded ${expr} for simplicity
|
|
49
|
+
text = init.quasis.map(q => q.value.cooked).join('');
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'StringLiteral':
|
|
53
|
+
text = init.value;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (text) {
|
|
57
|
+
const match = text.match(/\b(query|mutation|fragment)\b/i);
|
|
58
|
+
if (match) {
|
|
59
|
+
found.push(text.trim());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchSchema(endpoint) {
|
|
70
|
+
const body = JSON.stringify({ query: getIntrospectionQuery() });
|
|
71
|
+
const res = await fetch(endpoint, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`Introspection query failed: ${res.statusText}`);
|
|
77
|
+
const { data, errors } = await res.json();
|
|
78
|
+
if (errors?.length) throw new Error(`Server returned errors: ${JSON.stringify(errors)}`);
|
|
79
|
+
return buildClientSchema(data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function validateGqlOperations(endpoint, operation) {
|
|
83
|
+
console.log(`\nValidating against endpoint: ${endpoint}`);
|
|
84
|
+
try {
|
|
85
|
+
const document = parse(operation);
|
|
86
|
+
const errors = validate(await fetchSchema(endpoint), document);
|
|
87
|
+
if (errors.length) {
|
|
88
|
+
console.error('❌ Operation is NOT valid for this schema:');
|
|
89
|
+
errors.forEach(e => console.error('-', e.message));
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
} else {
|
|
92
|
+
console.log('✅ Operation is valid!');
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(e);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getAllOperations(directories) {
|
|
101
|
+
let fullContent = '';
|
|
102
|
+
for (const directory of directories) {
|
|
103
|
+
const files = await walk(path.resolve(directory));
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
const code = await fsPromises.readFile(f, 'utf8');
|
|
106
|
+
|
|
107
|
+
let extracted;
|
|
108
|
+
try {
|
|
109
|
+
extracted = extractConstants(code); // may throw on bad syntax
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(
|
|
112
|
+
`⚠️ Skipping ${path.relative(process.cwd(), f)}\n` +
|
|
113
|
+
` ${err.message}`
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
fullContent += extracted;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return fullContent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
module.exports = async function main(sources, endpoints) {
|
|
126
|
+
for (const endpoint of endpoints) {
|
|
127
|
+
const operations = await getAllOperations(sources);
|
|
128
|
+
if (!operations) {
|
|
129
|
+
console.error('No GraphQL operations found in the specified directories.');
|
|
130
|
+
process.exitCode = 0;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await validateGqlOperations(endpoint, operations);
|
|
134
|
+
}
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe-commerce/elsie",
|
|
3
|
-
"version": "1.4.0-
|
|
3
|
+
"version": "1.4.0-alpha3",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"description": "Domain Package SDK",
|
|
6
6
|
"engines": {
|
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@babel/core": "^7.24.9",
|
|
39
|
+
"@babel/parser": "^7.24.0",
|
|
39
40
|
"@babel/preset-env": "^7.24.8",
|
|
40
41
|
"@babel/preset-typescript": "^7.24.7",
|
|
42
|
+
"@babel/traverse": "^7.24.0",
|
|
41
43
|
"@chromatic-com/storybook": "^1",
|
|
42
44
|
"@graphql-codegen/cli": "^5.0.0",
|
|
43
45
|
"@graphql-codegen/client-preset": "^4.1.0",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/* Primary */
|
|
58
|
-
.dropin-button--primary,
|
|
59
|
-
a.dropin-button--primary,
|
|
58
|
+
button.dropin-button.dropin-button--primary,
|
|
59
|
+
a.dropin-button.dropin-button--primary,
|
|
60
60
|
.dropin-iconButton--primary {
|
|
61
61
|
border: none;
|
|
62
62
|
background: var(--color-brand-500) 0 0% no-repeat padding-box;
|
|
@@ -72,8 +72,8 @@ a.dropin-button--primary,
|
|
|
72
72
|
padding: var(--spacing-xsmall);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
.dropin-button--primary--disabled,
|
|
76
|
-
a.dropin-button--primary--disabled,
|
|
75
|
+
button.dropin-button.dropin-button--primary--disabled,
|
|
76
|
+
a.dropin-button.dropin-button--primary--disabled,
|
|
77
77
|
.dropin-iconButton--primary--disabled {
|
|
78
78
|
background: var(--color-neutral-300) 0 0% no-repeat padding-box;
|
|
79
79
|
color: var(--color-neutral-500);
|
|
@@ -82,21 +82,21 @@ a.dropin-button--primary--disabled,
|
|
|
82
82
|
user-select: none;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
.dropin-button--primary:hover,
|
|
86
|
-
a.dropin-button--primary:hover,
|
|
85
|
+
button.dropin-button.dropin-button--primary:hover,
|
|
86
|
+
a.dropin-button.dropin-button--primary:hover,
|
|
87
87
|
.dropin-iconButton--primary:hover,
|
|
88
|
-
.dropin-button--primary:focus:hover,
|
|
88
|
+
button.dropin-button.dropin-button--primary:focus:hover,
|
|
89
89
|
.dropin-iconButton--primary:focus:hover {
|
|
90
90
|
background-color: var(--color-button-hover);
|
|
91
91
|
text-decoration: none;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
.dropin-button--primary:focus,
|
|
94
|
+
button.dropin-button.dropin-button--primary:focus,
|
|
95
95
|
.dropin-iconButton--primary:focus {
|
|
96
96
|
background-color: var(--color-brand-500);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
.dropin-button--primary:hover:active,
|
|
99
|
+
button.dropin-button.dropin-button--primary:hover:active,
|
|
100
100
|
.dropin-iconButton--primary:hover:active {
|
|
101
101
|
background-color: var(--color-button-active);
|
|
102
102
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Copyright 2024 Adobe
|
|
3
3
|
* All Rights Reserved.
|
|
4
4
|
*
|
|
5
|
-
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
-
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
-
* accompanying it.
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
10
|
import { FunctionComponent } from 'preact';
|
|
@@ -86,7 +86,13 @@ function isValidUrl(source: string): boolean { // check for URL from same domain
|
|
|
86
86
|
if (source.startsWith('//')) {
|
|
87
87
|
const absoluteUrl = `${window.location.protocol}${source}`;
|
|
88
88
|
const url = new URL(absoluteUrl);
|
|
89
|
-
|
|
89
|
+
if (url.hostname !== window.location.hostname) {
|
|
90
|
+
console.error(
|
|
91
|
+
`[Icon] External URL rejected for security: ${source} - Only same-domain URLs are allowed`
|
|
92
|
+
);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
90
96
|
}
|
|
91
97
|
const url = new URL(source);
|
|
92
98
|
|
|
@@ -225,7 +231,14 @@ export function Icon({
|
|
|
225
231
|
viewBox,
|
|
226
232
|
};
|
|
227
233
|
|
|
228
|
-
|
|
234
|
+
// Only validate strings that look like URLs (start with http, //, or /)
|
|
235
|
+
const isLikelyUrl =
|
|
236
|
+
typeof Source === 'string' &&
|
|
237
|
+
(Source.startsWith('http') ||
|
|
238
|
+
Source.startsWith('//') ||
|
|
239
|
+
Source.startsWith('/'));
|
|
240
|
+
|
|
241
|
+
if (isLikelyUrl && isValidUrl(Source)) {
|
|
229
242
|
return (
|
|
230
243
|
<Suspense fallback={<svg {...props} {...defaultProps} />}>
|
|
231
244
|
<UrlSvgLoader url={Source} {...props} {...defaultProps}/>
|
|
@@ -237,8 +250,7 @@ export function Icon({
|
|
|
237
250
|
? lazyIcons[Source as IconType]
|
|
238
251
|
: null;
|
|
239
252
|
|
|
240
|
-
const isRejectedUrl =
|
|
241
|
-
(Source.startsWith('http') || Source.startsWith('//') || Source.startsWith('/'));
|
|
253
|
+
const isRejectedUrl = isLikelyUrl && !isValidUrl(Source);
|
|
242
254
|
|
|
243
255
|
return (
|
|
244
256
|
<Suspense fallback={<svg {...props} {...defaultProps} />}>
|
|
@@ -12,6 +12,7 @@ import { Modal as component, ModalProps } from './Modal';
|
|
|
12
12
|
import { useState } from 'preact/hooks';
|
|
13
13
|
import { Button } from '../Button';
|
|
14
14
|
import { expect, userEvent, within, waitFor } from '@storybook/test';
|
|
15
|
+
import { useText } from '@adobe-commerce/elsie/i18n';
|
|
15
16
|
|
|
16
17
|
const meta: Meta<ModalProps> = {
|
|
17
18
|
title: 'Components/Modal',
|
|
@@ -269,4 +270,65 @@ export const OverflowingTitle: Story = {
|
|
|
269
270
|
},
|
|
270
271
|
};
|
|
271
272
|
|
|
273
|
+
const LocalizedContent = () => {
|
|
274
|
+
const translations = useText({
|
|
275
|
+
label: 'Dropin.ExampleComponentName.item.label',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
console.log(translations.label);
|
|
279
|
+
return <div>{translations.label}</div>;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* ```ts
|
|
284
|
+
* import { Modal } from '@/elsie/components/Modal';
|
|
285
|
+
* import { useText } from '@adobe-commerce/elsie/i18n';
|
|
286
|
+
*
|
|
287
|
+
* const label = useText(`Dropin.ExampleComponentName.item.label`).label;
|
|
288
|
+
*
|
|
289
|
+
* <Modal size="medium" title={<h3>Localized Modal</h3>}>
|
|
290
|
+
* <div>{label}</div>
|
|
291
|
+
* </Modal>
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
export const LocalizedModal: Story = {
|
|
296
|
+
args: {
|
|
297
|
+
size: 'medium',
|
|
298
|
+
children: <LocalizedContent />,
|
|
299
|
+
title: <h3>Localized Modal</h3>,
|
|
300
|
+
},
|
|
301
|
+
play: async ({ canvasElement }) => {
|
|
302
|
+
const canvas = within(canvasElement);
|
|
303
|
+
await userEvent.click(canvas.getByRole('button'));
|
|
304
|
+
|
|
305
|
+
const portalRoot = await waitFor(() => {
|
|
306
|
+
const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
|
|
307
|
+
expect(root).toBeTruthy();
|
|
308
|
+
return root;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await expect(portalRoot).toBeVisible();
|
|
312
|
+
|
|
313
|
+
const modal = document.querySelector(
|
|
314
|
+
'.dropin-modal__body'
|
|
315
|
+
) as HTMLDivElement;
|
|
316
|
+
|
|
317
|
+
await expect(modal).toBeVisible();
|
|
318
|
+
|
|
319
|
+
expect(portalRoot.querySelector('h3')?.innerText).toBe('Localized Modal');
|
|
320
|
+
expect((portalRoot.querySelector('.dropin-modal__body') as HTMLElement)?.innerText).toContain('string');
|
|
321
|
+
|
|
322
|
+
const closeButton = document.querySelector(
|
|
323
|
+
'.dropin-modal__header-close-button'
|
|
324
|
+
) as HTMLButtonElement;
|
|
325
|
+
|
|
326
|
+
await userEvent.click(closeButton);
|
|
327
|
+
|
|
328
|
+
await expect(modal).not.toBeVisible();
|
|
329
|
+
|
|
330
|
+
await expect(canvas.getByText('Open Modal')).toBeVisible();
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
272
334
|
export default meta;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* accompanying it.
|
|
8
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
|
-
import { ComponentChildren
|
|
11
|
-
import { FunctionComponent,
|
|
10
|
+
import { ComponentChildren } from 'preact';
|
|
11
|
+
import { FunctionComponent, useLayoutEffect, useRef } from 'preact/compat';
|
|
12
12
|
|
|
13
13
|
interface PortalProps {
|
|
14
14
|
children: ComponentChildren;
|
|
@@ -16,28 +16,34 @@ interface PortalProps {
|
|
|
16
16
|
|
|
17
17
|
export const Portal: FunctionComponent<PortalProps> = ({ children }) => {
|
|
18
18
|
const portalRoot = useRef<HTMLDivElement | null>(null);
|
|
19
|
+
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
useLayoutEffect(() => {
|
|
21
22
|
// Create portal root if it doesn't exist
|
|
22
23
|
if (!portalRoot.current) {
|
|
23
24
|
portalRoot.current = document.createElement('div');
|
|
24
25
|
portalRoot.current.setAttribute('data-portal-root', '');
|
|
25
|
-
portalRoot.current.classList.add('dropin-design');
|
|
26
26
|
document.body.appendChild(portalRoot.current);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
|
|
29
|
+
// Move content to portal root
|
|
30
|
+
if (contentRef.current && portalRoot.current) {
|
|
31
|
+
portalRoot.current.appendChild(contentRef.current);
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
// Cleanup
|
|
33
35
|
return () => {
|
|
34
36
|
if (portalRoot.current) {
|
|
35
|
-
render(null, portalRoot.current);
|
|
36
37
|
portalRoot.current.remove();
|
|
37
38
|
portalRoot.current = null;
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
|
-
}, [
|
|
41
|
+
}, []);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Return a div that contains the children
|
|
44
|
+
return (
|
|
45
|
+
<div ref={contentRef} className="dropin-design">
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
43
49
|
};
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import { FunctionComponent } from 'preact';
|
|
11
11
|
import { HTMLAttributes, useMemo } from 'preact/compat';
|
|
12
|
-
import { classes } from '@adobe-commerce/elsie/lib';
|
|
12
|
+
import { classes, getGlobalLocale } from '@adobe-commerce/elsie/lib';
|
|
13
13
|
import '@adobe-commerce/elsie/components/Price/Price.css';
|
|
14
14
|
|
|
15
15
|
export interface PriceProps
|
|
16
16
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'size'> {
|
|
17
17
|
amount?: number;
|
|
18
|
-
currency?: string;
|
|
18
|
+
currency?: string | null;
|
|
19
19
|
locale?: string;
|
|
20
20
|
formatOptions?: {
|
|
21
21
|
[key: string]: any;
|
|
@@ -29,7 +29,7 @@ export interface PriceProps
|
|
|
29
29
|
export const Price: FunctionComponent<PriceProps> = ({
|
|
30
30
|
amount = 0,
|
|
31
31
|
currency,
|
|
32
|
-
locale
|
|
32
|
+
locale,
|
|
33
33
|
variant = 'default',
|
|
34
34
|
weight = 'bold',
|
|
35
35
|
className,
|
|
@@ -39,17 +39,37 @@ export const Price: FunctionComponent<PriceProps> = ({
|
|
|
39
39
|
size = 'small',
|
|
40
40
|
...props
|
|
41
41
|
}) => {
|
|
42
|
+
// Determine the locale to use: prop locale > global locale > browser locale
|
|
43
|
+
const effectiveLocale = useMemo(() => {
|
|
44
|
+
if (locale) {
|
|
45
|
+
return locale;
|
|
46
|
+
}
|
|
47
|
+
const globalLocale = getGlobalLocale();
|
|
48
|
+
if (globalLocale) {
|
|
49
|
+
return globalLocale;
|
|
50
|
+
}
|
|
51
|
+
// Fallback to browser locale or default
|
|
52
|
+
return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
|
|
53
|
+
}, [locale]);
|
|
54
|
+
|
|
42
55
|
const formatter = useMemo(
|
|
43
|
-
() =>
|
|
44
|
-
|
|
56
|
+
() => {
|
|
57
|
+
const params: Intl.NumberFormatOptions = {
|
|
45
58
|
style: 'currency',
|
|
46
59
|
currency: currency || 'USD',
|
|
47
60
|
// These options are needed to round to whole numbers if that's what you want.
|
|
48
61
|
minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
|
|
49
62
|
maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
|
|
50
63
|
...formatOptions,
|
|
51
|
-
}
|
|
52
|
-
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return new Intl.NumberFormat(effectiveLocale, params);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
|
|
69
|
+
return new Intl.NumberFormat('en-US', params);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[effectiveLocale, currency, formatOptions]
|
|
53
73
|
);
|
|
54
74
|
|
|
55
75
|
const formattedAmount = useMemo(
|
|
@@ -81,13 +81,9 @@ initializers.setImageParamKeys({
|
|
|
81
81
|
extraParam: () => ['extraParam', 'extraValue'],
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
langDefinitions,
|
|
84
|
+
initializers.mountImmediately(pkg.initialize, {
|
|
85
|
+
langDefinitions
|
|
87
86
|
});
|
|
88
|
-
|
|
89
|
-
// Mount Initializers
|
|
90
|
-
initializers.mount();
|
|
91
87
|
```
|
|
92
88
|
|
|
93
89
|
Now, when a dropin uses the Image component to render an image with a width of 300 pixels and quality value of 0.8:
|
|
@@ -116,4 +112,70 @@ It renders the following image element:
|
|
|
116
112
|
/>
|
|
117
113
|
```
|
|
118
114
|
|
|
119
|
-
In this example, the width parameter is mapped to imgWidth and the value of the quality parameter is modified and mapped to imgQuality.
|
|
115
|
+
In this example, the width parameter is mapped to imgWidth and the value of the quality parameter is modified and mapped to imgQuality.
|
|
116
|
+
|
|
117
|
+
## `setGlobalLocale(locale)`
|
|
118
|
+
|
|
119
|
+
The `setGlobalLocale` method is part of the initializers module in the `@dropins/tools` package.
|
|
120
|
+
It allows you to set a global locale for all drop-ins that use locale-sensitive components like the Price component.
|
|
121
|
+
|
|
122
|
+
### Default Behavior
|
|
123
|
+
|
|
124
|
+
By default, components use the browser's locale or fallback to 'en-US' if no global locale is set.
|
|
125
|
+
|
|
126
|
+
### Parameters
|
|
127
|
+
|
|
128
|
+
- `locale` - `string` - The locale string (e.g., 'en-US', 'es-MX', 'fr-FR', 'de-DE').
|
|
129
|
+
|
|
130
|
+
### Functionality
|
|
131
|
+
|
|
132
|
+
- If a global locale is set via `setGlobalLocale`, it will be used by components that support locale configuration.
|
|
133
|
+
- Component-specific locale props will take precedence over the global locale.
|
|
134
|
+
- If no global locale is set, components will fall back to the browser's locale or a default locale.
|
|
135
|
+
|
|
136
|
+
### Usage
|
|
137
|
+
|
|
138
|
+
Call the `setGlobalLocale()` function before the `mountImmediately()` function in the application layer.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
// Set global locale for consistent formatting across all drop-ins
|
|
142
|
+
initializers.setGlobalLocale('fr-FR');
|
|
143
|
+
|
|
144
|
+
// Register and Mount Initializers immediately
|
|
145
|
+
initializers.mountImmediately(pkg.initialize, {});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Now, when a dropin uses the Price component without specifying a locale prop:
|
|
149
|
+
|
|
150
|
+
```jsx
|
|
151
|
+
<Price
|
|
152
|
+
amount={100}
|
|
153
|
+
currency="EUR"
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
It will render with the global locale (fr-FR) formatting:
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<span class="dropin-price dropin-price--default dropin-price--small dropin-price--bold">
|
|
161
|
+
100,00 €
|
|
162
|
+
</span>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
If the same component is used with a specific locale prop, that will take precedence:
|
|
166
|
+
|
|
167
|
+
```jsx
|
|
168
|
+
<Price
|
|
169
|
+
amount={100}
|
|
170
|
+
currency="EUR"
|
|
171
|
+
locale="en-US"
|
|
172
|
+
/>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
It will render with the specified locale (en-US) formatting:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<span class="dropin-price dropin-price--default dropin-price--small dropin-price--bold">
|
|
179
|
+
€100.00
|
|
180
|
+
</span>
|
|
181
|
+
```
|
package/src/lib/aem/assets.ts
CHANGED
|
@@ -1,56 +1,102 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
provider as UI,
|
|
3
|
+
Image,
|
|
4
|
+
type ImageProps,
|
|
5
|
+
} from '@adobe-commerce/elsie/components';
|
|
6
|
+
|
|
2
7
|
import { getConfigValue } from '@adobe-commerce/elsie/lib/aem/configs';
|
|
8
|
+
import type { ResolveImageUrlOptions } from '../resolve-image';
|
|
3
9
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const AEM_ASSETS_FORMATS = ['gif', 'jpg', 'jpeg', 'png', 'webp'] as const;
|
|
11
|
+
const AEM_ASSETS_ALLOWED_ROTATIONS = [90, 180, 270] as const;
|
|
12
|
+
const AEM_ASSETS_ALLOWED_FLIPS = ['h', 'v', 'hv'] as const;
|
|
13
|
+
|
|
14
|
+
/** The allowed formats for the `AEM Assets` image optimization API. */
|
|
15
|
+
export type AemAssetsFormat = (typeof AEM_ASSETS_FORMATS)[number];
|
|
16
|
+
|
|
17
|
+
/** The allowed rotations for the `AEM Assets` image optimization API. */
|
|
18
|
+
export type AemAssetsRotation = (typeof AEM_ASSETS_ALLOWED_ROTATIONS)[number];
|
|
19
|
+
|
|
20
|
+
/** The allowed flips for the `AEM Assets` image optimization API. */
|
|
21
|
+
export type AemAssetsFlip = (typeof AEM_ASSETS_ALLOWED_FLIPS)[number];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Defines a crop region of an image.
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* // Crop the image to a 80% width and height, starting at 10% from the top and left.
|
|
28
|
+
* const cropSettings: AemAssetsCropSettings = {
|
|
29
|
+
* xOrigin: 10,
|
|
30
|
+
* yOrigin: 10,
|
|
31
|
+
* width: 80,
|
|
32
|
+
* height: 80,
|
|
33
|
+
* };
|
|
34
|
+
*/
|
|
35
|
+
export interface AemAssetsCropSettings {
|
|
36
|
+
/** The (relative) x origin of the crop (between 0 and 100) */
|
|
37
|
+
xOrigin?: number;
|
|
38
|
+
|
|
39
|
+
/** The (relative) y origin of the crop (between 0 and 100) */
|
|
40
|
+
yOrigin?: number;
|
|
41
|
+
|
|
42
|
+
/** The width of the crop (between 0 and 100) */
|
|
17
43
|
width?: number;
|
|
44
|
+
|
|
45
|
+
/** The height of the crop (between 0 and 100) */
|
|
18
46
|
height?: number;
|
|
19
|
-
[key: string]: any;
|
|
20
47
|
}
|
|
21
48
|
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The parameters accepted by the `AEM Assets` image optimization API.
|
|
51
|
+
* @see https://adobe-aem-assets-delivery-experimental.redoc.ly/
|
|
52
|
+
*/
|
|
53
|
+
export interface AemAssetsParams {
|
|
54
|
+
format: AemAssetsFormat;
|
|
55
|
+
rotate?: AemAssetsRotation;
|
|
56
|
+
flip?: AemAssetsFlip;
|
|
57
|
+
crop?: AemAssetsCropSettings;
|
|
58
|
+
|
|
59
|
+
width?: number;
|
|
60
|
+
height?: number;
|
|
61
|
+
quality?: number;
|
|
62
|
+
|
|
63
|
+
attachment?: boolean;
|
|
64
|
+
sharpen?: boolean;
|
|
65
|
+
blur?: number;
|
|
66
|
+
dpr?: number;
|
|
67
|
+
smartCrop?: string;
|
|
68
|
+
|
|
69
|
+
// For future updates we may miss.
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
|
74
|
+
|
|
75
|
+
/** The parameters to be applied to the asset (known width required when using a slot) */
|
|
76
|
+
export type AemAssetsImageSlotConfigParams = WithRequired<
|
|
77
|
+
Partial<AemAssetsParams>,
|
|
78
|
+
'width'
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
/** The configuration for an image slot. */
|
|
82
|
+
export interface AemAssetsImageSlotConfig {
|
|
83
|
+
/** The alias (i.e. seoName) of the image */
|
|
24
84
|
alias: string;
|
|
25
|
-
|
|
26
|
-
|
|
85
|
+
|
|
86
|
+
/** The props to be applied to the underlying {@link Image} component */
|
|
87
|
+
imageProps: Partial<Omit<ImageProps, 'params' | 'width' | 'height'>> & {
|
|
27
88
|
src: string;
|
|
28
|
-
width?: number;
|
|
29
|
-
height?: number;
|
|
30
|
-
[key: string]: any;
|
|
31
89
|
};
|
|
32
|
-
src?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface RenderContext {
|
|
36
|
-
replaceWith: (element: HTMLElement) => void;
|
|
37
|
-
}
|
|
38
90
|
|
|
39
|
-
|
|
40
|
-
|
|
91
|
+
/** The parameters to be applied to the asset (known width required when using a slot) */
|
|
92
|
+
params: AemAssetsImageSlotConfigParams;
|
|
41
93
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|| (typeof config === 'boolean' && config === true)
|
|
45
|
-
);
|
|
94
|
+
/** The element that will contain the image in the slot */
|
|
95
|
+
wrapper?: HTMLElement;
|
|
46
96
|
}
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
quality: 80,
|
|
52
|
-
format: 'webp',
|
|
53
|
-
};
|
|
98
|
+
interface RenderContext {
|
|
99
|
+
replaceWith: (element: HTMLElement) => void;
|
|
54
100
|
}
|
|
55
101
|
|
|
56
102
|
/**
|
|
@@ -64,13 +110,59 @@ function normalizeUrl(url: string): string {
|
|
|
64
110
|
if (imageUrl.startsWith('//')) {
|
|
65
111
|
// Use current window's protocol.
|
|
66
112
|
const { protocol } = window.location;
|
|
67
|
-
console.log('protocol', protocol);
|
|
68
113
|
imageUrl = protocol + imageUrl;
|
|
69
114
|
}
|
|
70
115
|
|
|
71
116
|
return imageUrl;
|
|
72
117
|
}
|
|
73
118
|
|
|
119
|
+
/** Returns whether the given value is a valid flip. */
|
|
120
|
+
function isValidFlip(flip: unknown): flip is AemAssetsFlip {
|
|
121
|
+
return AEM_ASSETS_ALLOWED_FLIPS.includes(flip as AemAssetsFlip);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Returns whether the given value is a valid rotation. */
|
|
125
|
+
function isValidRotation(rotation: unknown): rotation is AemAssetsRotation {
|
|
126
|
+
return AEM_ASSETS_ALLOWED_ROTATIONS.includes(rotation as AemAssetsRotation);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Returns whether the given value is a valid format. */
|
|
130
|
+
function isValidFormat(format: unknown): format is AemAssetsFormat {
|
|
131
|
+
return AEM_ASSETS_FORMATS.includes(format as AemAssetsFormat);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Asserts that the given value is valid. */
|
|
135
|
+
function assertUnionParameter(
|
|
136
|
+
value: unknown,
|
|
137
|
+
validator: (value: unknown) => boolean,
|
|
138
|
+
errorMessage: string
|
|
139
|
+
): void {
|
|
140
|
+
if (value !== undefined && !validator(value)) {
|
|
141
|
+
throw new Error(errorMessage);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Returns whether AEM Assets is enabled in the Storefront. */
|
|
146
|
+
export function isAemAssetsEnabled(): boolean {
|
|
147
|
+
const config = getConfigValue('commerce-assets-enabled');
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
config &&
|
|
151
|
+
((typeof config === 'string' && config.toLowerCase() === 'true') ||
|
|
152
|
+
(typeof config === 'boolean' && config === true))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The default optimization parameters used globally, unless overriden (per use). */
|
|
157
|
+
export function getDefaultAemAssetsOptimizationParams(): AemAssetsParams {
|
|
158
|
+
// See: https://adobe-aem-assets-delivery-experimental.redoc.ly/
|
|
159
|
+
return {
|
|
160
|
+
quality: 80,
|
|
161
|
+
format: 'webp',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Returns true if the given URL is an AEM Assets URL. */
|
|
74
166
|
export function isAemAssetsUrl(url: string | URL): boolean {
|
|
75
167
|
const assetsUrl = typeof url === 'string' ? new URL(normalizeUrl(url)) : url;
|
|
76
168
|
|
|
@@ -81,19 +173,30 @@ export function isAemAssetsUrl(url: string | URL): boolean {
|
|
|
81
173
|
return true;
|
|
82
174
|
}
|
|
83
175
|
|
|
84
|
-
|
|
176
|
+
/** Generates an optimized URL for AEM Assets. */
|
|
177
|
+
export function generateAemAssetsOptimizedUrl(
|
|
178
|
+
assetUrl: string,
|
|
179
|
+
alias: string,
|
|
180
|
+
params: Partial<AemAssetsParams> = {}
|
|
181
|
+
): string {
|
|
85
182
|
const defaultParams = getDefaultAemAssetsOptimizationParams();
|
|
86
183
|
const mergedParams: AemAssetsParams = { ...defaultParams, ...params };
|
|
87
184
|
|
|
88
|
-
// Destructure the ones that need special handling
|
|
89
|
-
const {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
185
|
+
// Destructure the ones that need special handling/validation.
|
|
186
|
+
const { format, crop, ...optimizedParams } = mergedParams;
|
|
187
|
+
assertUnionParameter(format, isValidFormat, 'Invalid format');
|
|
188
|
+
assertUnionParameter(optimizedParams.flip, isValidFlip, 'Invalid flip');
|
|
189
|
+
assertUnionParameter(
|
|
190
|
+
optimizedParams.rotate,
|
|
191
|
+
isValidRotation,
|
|
192
|
+
'Invalid rotation'
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const stringifiedParams = Object.fromEntries(
|
|
196
|
+
Object.entries(optimizedParams).map(([key, value]) => [key, String(value)])
|
|
197
|
+
);
|
|
95
198
|
|
|
96
|
-
const searchParams = new URLSearchParams(
|
|
199
|
+
const searchParams = new URLSearchParams(stringifiedParams);
|
|
97
200
|
|
|
98
201
|
if (crop) {
|
|
99
202
|
const [xOrigin, yOrigin] = [crop.xOrigin || 0, crop.yOrigin || 0];
|
|
@@ -103,87 +206,112 @@ export function generateAemAssetsOptimizedUrl(url: string, alias: string, params
|
|
|
103
206
|
searchParams.set('crop', cropTransform);
|
|
104
207
|
}
|
|
105
208
|
|
|
106
|
-
|
|
107
|
-
if (size && size.width && size.height) {
|
|
108
|
-
searchParams.set('size', `${size.width},${size.height}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return `${url}/as/${alias}.${format}?${searchParams.toString()}`;
|
|
209
|
+
return `${assetUrl}/as/${alias}.${format}?${searchParams.toString()}`;
|
|
112
210
|
}
|
|
113
211
|
|
|
114
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Tries to generate an optimized URL for AEM Assets. Returns the given
|
|
214
|
+
* url if AEM Assets is not enabled or is not an AEM Assets URL.
|
|
215
|
+
*/
|
|
216
|
+
export function tryGenerateAemAssetsOptimizedUrl(
|
|
217
|
+
assetUrl: string,
|
|
218
|
+
alias: string,
|
|
219
|
+
params: Partial<AemAssetsParams> = {}
|
|
220
|
+
): string {
|
|
115
221
|
const assetsEnabled = isAemAssetsEnabled();
|
|
116
222
|
|
|
117
|
-
if (!
|
|
223
|
+
if (!assetsEnabled) {
|
|
118
224
|
// No-op, doesn't do anything.
|
|
119
|
-
return
|
|
225
|
+
return assetUrl;
|
|
120
226
|
}
|
|
121
227
|
|
|
122
|
-
const assetsUrl = new URL(normalizeUrl(
|
|
228
|
+
const assetsUrl = new URL(normalizeUrl(assetUrl));
|
|
123
229
|
|
|
124
230
|
if (!isAemAssetsUrl(assetsUrl)) {
|
|
125
231
|
// Not an AEM Assets URL, so no-op.
|
|
126
|
-
return
|
|
232
|
+
return assetUrl;
|
|
127
233
|
}
|
|
128
234
|
|
|
129
235
|
const base = assetsUrl.origin + assetsUrl.pathname;
|
|
130
236
|
return generateAemAssetsOptimizedUrl(base, alias, params);
|
|
131
237
|
}
|
|
132
238
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
) {
|
|
239
|
+
/** Creates a slot that renders an AEM Assets image. */
|
|
240
|
+
export function makeAemAssetsImageSlot(config: AemAssetsImageSlotConfig) {
|
|
136
241
|
return (ctx: RenderContext) => {
|
|
137
|
-
const {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
242
|
+
const { wrapper, alias, params, imageProps } = config;
|
|
243
|
+
|
|
244
|
+
if (!imageProps.src) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'An image source is required. Please provide a `src` or `imageProps.src`.'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
144
249
|
|
|
145
250
|
const container = wrapper ?? document.createElement('div');
|
|
146
|
-
const imageSrc = generateAemAssetsOptimizedUrl(
|
|
251
|
+
const imageSrc = generateAemAssetsOptimizedUrl(
|
|
252
|
+
imageProps.src,
|
|
253
|
+
alias,
|
|
254
|
+
params
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const imageComponentParams: ResolveImageUrlOptions = {
|
|
258
|
+
width: params.width,
|
|
259
|
+
height: params.height,
|
|
147
260
|
|
|
148
|
-
|
|
261
|
+
// If this is not done, they will be applied by default.
|
|
262
|
+
// And they are not compatible with the AEM Assets API.
|
|
263
|
+
crop: undefined,
|
|
264
|
+
fit: undefined,
|
|
265
|
+
auto: undefined,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const imageComponentProps: ImageProps = {
|
|
149
269
|
...imageProps,
|
|
270
|
+
width: params.width,
|
|
271
|
+
height: params.height,
|
|
150
272
|
|
|
151
273
|
src: imageSrc,
|
|
152
|
-
params:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// If not null, they will be applied by default.
|
|
156
|
-
// And they are not compatible with the AEM Assets API.
|
|
157
|
-
crop: null,
|
|
158
|
-
fit: null,
|
|
159
|
-
auto: null,
|
|
160
|
-
},
|
|
161
|
-
})(container);
|
|
274
|
+
params: imageComponentParams,
|
|
275
|
+
};
|
|
162
276
|
|
|
277
|
+
UI.render(Image, imageComponentProps)(container);
|
|
163
278
|
ctx.replaceWith(container);
|
|
164
279
|
};
|
|
165
280
|
}
|
|
166
281
|
|
|
167
|
-
export function tryRenderAemAssetsImage(
|
|
282
|
+
export function tryRenderAemAssetsImage(
|
|
283
|
+
ctx: RenderContext,
|
|
284
|
+
config: AemAssetsImageSlotConfig
|
|
285
|
+
): void {
|
|
168
286
|
// Renders an equivalent of the default image.
|
|
169
287
|
function renderDefaultImage(): void {
|
|
170
288
|
const container = config.wrapper ?? document.createElement('div');
|
|
171
|
-
const { imageProps } = config;
|
|
289
|
+
const { imageProps, params } = config;
|
|
290
|
+
const imageComponentProps: ImageProps = {
|
|
291
|
+
...imageProps,
|
|
292
|
+
width: params.width,
|
|
293
|
+
height: params.height,
|
|
294
|
+
};
|
|
172
295
|
|
|
173
|
-
|
|
296
|
+
UI.render(Image, imageComponentProps)(container);
|
|
174
297
|
ctx.replaceWith(container);
|
|
175
298
|
}
|
|
176
299
|
|
|
177
300
|
const assetsEnabled = isAemAssetsEnabled();
|
|
178
301
|
|
|
179
|
-
if (!
|
|
302
|
+
if (!assetsEnabled) {
|
|
180
303
|
// No-op, render the default image.
|
|
181
304
|
renderDefaultImage();
|
|
182
305
|
return;
|
|
183
306
|
}
|
|
184
307
|
|
|
185
|
-
|
|
186
|
-
|
|
308
|
+
if (!config.imageProps.src) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
'An image source is required. Please provide a `src` or `imageProps.src`.'
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const assetsUrl = new URL(normalizeUrl(config.imageProps.src));
|
|
187
315
|
|
|
188
316
|
if (!isAemAssetsUrl(assetsUrl)) {
|
|
189
317
|
// Not an AEM Assets URL, so render the default image.
|
|
@@ -191,17 +319,5 @@ export function tryRenderAemAssetsImage(ctx: RenderContext, config: AemAssetsIma
|
|
|
191
319
|
return;
|
|
192
320
|
}
|
|
193
321
|
|
|
194
|
-
makeAemAssetsImageSlot(
|
|
195
|
-
// Use the default image props for params and src.
|
|
196
|
-
// Unless overriden by the slot config.
|
|
197
|
-
src: assetsUrl.toString(),
|
|
198
|
-
params: {
|
|
199
|
-
width: imageProps.width,
|
|
200
|
-
height: imageProps.height,
|
|
201
|
-
...slotConfig.params,
|
|
202
|
-
},
|
|
203
|
-
imageProps,
|
|
204
|
-
alias: slotConfig.alias,
|
|
205
|
-
wrapper: slotConfig.wrapper,
|
|
206
|
-
})(ctx);
|
|
322
|
+
makeAemAssetsImageSlot(config)(ctx);
|
|
207
323
|
}
|
package/src/lib/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from '@adobe-commerce/elsie/lib/types';
|
|
|
20
20
|
export * from '@adobe-commerce/elsie/lib/slot';
|
|
21
21
|
export * from '@adobe-commerce/elsie/lib/vcomponent';
|
|
22
22
|
export * from '@adobe-commerce/elsie/lib/image-params-keymap';
|
|
23
|
+
export * from '@adobe-commerce/elsie/lib/locale-config';
|
|
23
24
|
export * from '@adobe-commerce/elsie/lib/is-number';
|
|
24
25
|
export * from '@adobe-commerce/elsie/lib/deviceUtils';
|
|
25
26
|
export * from '@adobe-commerce/elsie/lib/get-path-value';
|
package/src/lib/initializer.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
Config,
|
|
12
12
|
setImageParamsKeyMap,
|
|
13
|
+
setGlobalLocale,
|
|
13
14
|
} from '@adobe-commerce/elsie/lib';
|
|
14
15
|
|
|
15
16
|
type Listener = { off(): void };
|
|
@@ -51,10 +52,11 @@ export class Initializer<T> {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
this.init = (options) => {
|
|
54
|
-
const { imageParamsKeyMap, ...rest } =
|
|
55
|
+
const { imageParamsKeyMap, globalLocale, ...rest } =
|
|
55
56
|
options as any;
|
|
56
57
|
this.config.setConfig({ ...this.config.getConfig(), ...rest });
|
|
57
58
|
setImageParamsKeyMap(imageParamsKeyMap);
|
|
59
|
+
setGlobalLocale(globalLocale);
|
|
58
60
|
return init(options);
|
|
59
61
|
};
|
|
60
62
|
}
|
|
@@ -75,6 +77,7 @@ export class initializers {
|
|
|
75
77
|
static _initializers: Initializers = [];
|
|
76
78
|
static _mounted: boolean = false;
|
|
77
79
|
static _imageParamsKeyMap: { [key: string]: string } | undefined = undefined;
|
|
80
|
+
static _globalLocale: string | undefined = undefined;
|
|
78
81
|
/**
|
|
79
82
|
* Registers a new initializer. If the initializers have already been mounted,it immediately binds the event listeners and initializes the API for the new initializer.
|
|
80
83
|
* @param initializer - The initializer to register.
|
|
@@ -99,10 +102,11 @@ export class initializers {
|
|
|
99
102
|
options?: { [key: string]: any }
|
|
100
103
|
) {
|
|
101
104
|
initializer.listeners?.(options);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
await initializer.init?.({
|
|
106
|
+
imageParamsKeyMap: initializers._imageParamsKeyMap,
|
|
107
|
+
globalLocale: initializers._globalLocale,
|
|
108
|
+
...options,
|
|
109
|
+
});
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
@@ -120,6 +124,7 @@ export class initializers {
|
|
|
120
124
|
initializers._initializers?.forEach(([initializer, options]) => {
|
|
121
125
|
initializer.init?.({
|
|
122
126
|
imageParamsKeyMap: initializers._imageParamsKeyMap,
|
|
127
|
+
globalLocale: initializers._globalLocale,
|
|
123
128
|
...options,
|
|
124
129
|
});
|
|
125
130
|
});
|
|
@@ -131,4 +136,12 @@ export class initializers {
|
|
|
131
136
|
static setImageParamKeys(params: { [key: string]: any }) {
|
|
132
137
|
initializers._imageParamsKeyMap = params;
|
|
133
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sets the global locale. This locale is used by components that need consistent formatting regardless of the user's browser locale.
|
|
142
|
+
* @param locale - The locale string (e.g., 'en-US', 'es-MX', 'fr-FR').
|
|
143
|
+
*/
|
|
144
|
+
static setGlobalLocale(locale: string) {
|
|
145
|
+
initializers._globalLocale = locale;
|
|
146
|
+
}
|
|
134
147
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2024 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
class LocaleConfig {
|
|
11
|
+
private _locale?: string | undefined;
|
|
12
|
+
|
|
13
|
+
get locale() {
|
|
14
|
+
return this._locale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
set locale(value: typeof this._locale) {
|
|
18
|
+
this._locale = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public getMethods() {
|
|
22
|
+
return {
|
|
23
|
+
setLocale: (value: typeof this._locale) => {
|
|
24
|
+
this.locale = value;
|
|
25
|
+
},
|
|
26
|
+
getLocale: () => this.locale,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const localeConfig = new LocaleConfig();
|
|
32
|
+
|
|
33
|
+
export const { setLocale: setGlobalLocale, getLocale: getGlobalLocale } =
|
|
34
|
+
localeConfig.getMethods();
|