@adobe-commerce/elsie 1.3.0-alpha068 → 1.3.0-alpha16
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/package.json +4 -4
- package/src/components/CartItem/CartItem.css +0 -9
- package/src/components/Icon/Icon.tsx +1 -0
- package/src/components/Modal/Modal.css +2 -2
- package/src/components/Modal/Modal.stories.tsx +26 -4
- package/src/components/Modal/Modal.tsx +40 -37
- package/src/components/Portal/Portal.stories.tsx +145 -0
- package/src/components/Portal/Portal.tsx +43 -0
- package/src/icons/Edit.svg +1 -0
- package/src/icons/index.ts +1 -0
- package/src/lib/aem/assets.ts +207 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe-commerce/elsie",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-alpha16",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"description": "Domain Package SDK",
|
|
6
6
|
"engines": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@adobe-commerce/event-bus": "~1.0.0",
|
|
30
|
-
"@adobe-commerce/fetch-graphql": "1.1.0
|
|
31
|
-
"@adobe-commerce/recaptcha": "1.0.1
|
|
30
|
+
"@adobe-commerce/fetch-graphql": "~1.1.0",
|
|
31
|
+
"@adobe-commerce/recaptcha": "~1.0.1",
|
|
32
32
|
"@adobe-commerce/storefront-design": "~1.0.0",
|
|
33
|
-
"@dropins/build-tools": "1.0.1
|
|
33
|
+
"@dropins/build-tools": "~1.0.1",
|
|
34
34
|
"preact": "~10.22.1",
|
|
35
35
|
"vite-plugin-banner": "^0.8.0"
|
|
36
36
|
},
|
|
@@ -241,15 +241,10 @@
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
.dropin-cart-item__price,
|
|
244
|
-
.dropin-cart-item__footer div,
|
|
245
244
|
.dropin-cart-item__attributes div {
|
|
246
245
|
margin-top: var(--group-spacing);
|
|
247
246
|
}
|
|
248
247
|
|
|
249
|
-
.dropin-cart-item__footer p {
|
|
250
|
-
margin-top: 0;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
248
|
.dropin-cart-item__quantity:not(.dropin-cart-item__quantity--edit) {
|
|
254
249
|
display: none;
|
|
255
250
|
}
|
|
@@ -306,10 +301,6 @@
|
|
|
306
301
|
grid-row: 5 / 10;
|
|
307
302
|
}
|
|
308
303
|
|
|
309
|
-
.dropin-cart-item__title--edit {
|
|
310
|
-
grid-column: 2 / 5;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
304
|
.dropin-cart-item__total:not(.dropin-cart-item__total--edit) {
|
|
314
305
|
grid-auto-rows: min-content;
|
|
315
306
|
grid-column: 4 / 5;
|
|
@@ -62,6 +62,7 @@ const lazyIcons = {
|
|
|
62
62
|
Coupon: lazy(() => import('@adobe-commerce/elsie/icons/Coupon.svg')),
|
|
63
63
|
Gift: lazy(() => import('@adobe-commerce/elsie/icons/Gift.svg')),
|
|
64
64
|
GiftCard: lazy(() => import('@adobe-commerce/elsie/icons/GiftCard.svg')),
|
|
65
|
+
Edit: lazy(() => import('@adobe-commerce/elsie/icons/Edit.svg')),
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'size'> {
|
|
@@ -11,7 +11,7 @@ import { Meta, StoryObj } from '@storybook/preact';
|
|
|
11
11
|
import { Modal as component, ModalProps } from './Modal';
|
|
12
12
|
import { useState } from 'preact/hooks';
|
|
13
13
|
import { Button } from '../Button';
|
|
14
|
-
import { expect, userEvent, within } from '@storybook/test';
|
|
14
|
+
import { expect, userEvent, within, waitFor } from '@storybook/test';
|
|
15
15
|
|
|
16
16
|
const meta: Meta<ModalProps> = {
|
|
17
17
|
title: 'Components/Modal',
|
|
@@ -107,13 +107,21 @@ export const SmallModal: Story = {
|
|
|
107
107
|
const canvas = within(canvasElement);
|
|
108
108
|
await userEvent.click(canvas.getByRole('button'));
|
|
109
109
|
|
|
110
|
+
const portalRoot = await waitFor(() => {
|
|
111
|
+
const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
|
|
112
|
+
expect(root).toBeTruthy();
|
|
113
|
+
return root;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(portalRoot).toBeVisible();
|
|
117
|
+
|
|
110
118
|
const modal = document.querySelector(
|
|
111
119
|
'.dropin-modal__body'
|
|
112
120
|
) as HTMLDivElement;
|
|
113
121
|
|
|
114
122
|
await expect(modal).toBeVisible();
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
expect(portalRoot.querySelector('h4')?.innerText).toBe('Small modal');
|
|
117
125
|
|
|
118
126
|
const closeButton = document.querySelector(
|
|
119
127
|
'.dropin-modal__header-close-button'
|
|
@@ -148,13 +156,21 @@ export const MediumModal: Story = {
|
|
|
148
156
|
const canvas = within(canvasElement);
|
|
149
157
|
await userEvent.click(canvas.getByRole('button'));
|
|
150
158
|
|
|
159
|
+
const portalRoot = await waitFor(() => {
|
|
160
|
+
const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
|
|
161
|
+
expect(root).toBeTruthy();
|
|
162
|
+
return root;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expect(portalRoot).toBeVisible();
|
|
166
|
+
|
|
151
167
|
const modal = document.querySelector(
|
|
152
168
|
'.dropin-modal__body'
|
|
153
169
|
) as HTMLDivElement;
|
|
154
170
|
|
|
155
171
|
await expect(modal).toBeVisible();
|
|
156
172
|
|
|
157
|
-
|
|
173
|
+
expect(portalRoot.querySelector('h3')?.innerText).toBe('Medium modal');
|
|
158
174
|
|
|
159
175
|
const closeButton = document.querySelector(
|
|
160
176
|
'.dropin-modal__header-close-button'
|
|
@@ -187,11 +203,17 @@ export const FullModal: Story = {
|
|
|
187
203
|
const canvas = within(canvasElement);
|
|
188
204
|
await userEvent.click(canvas.getByRole('button'));
|
|
189
205
|
|
|
206
|
+
const portalRoot = await waitFor(() => {
|
|
207
|
+
const root = document.querySelector('[data-portal-root]') as HTMLDivElement;
|
|
208
|
+
expect(root).toBeTruthy();
|
|
209
|
+
return root;
|
|
210
|
+
});
|
|
211
|
+
|
|
190
212
|
const modal = document.querySelector(
|
|
191
213
|
'.dropin-modal__body'
|
|
192
214
|
) as HTMLDivElement;
|
|
193
215
|
|
|
194
|
-
await expect(
|
|
216
|
+
await expect(portalRoot).toBeVisible();
|
|
195
217
|
|
|
196
218
|
const closeButton = document.querySelector(
|
|
197
219
|
'.dropin-modal__header-close-button'
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { Button } from '../Button';
|
|
19
19
|
import { Close as CloseSVG } from '@adobe-commerce/elsie/icons';
|
|
20
20
|
import { VNode } from 'preact';
|
|
21
|
+
import { Portal } from '../Portal/Portal';
|
|
21
22
|
|
|
22
23
|
import '@adobe-commerce/elsie/components/Modal/Modal.css';
|
|
23
24
|
|
|
@@ -106,52 +107,54 @@ export const Modal: FunctionComponent<ModalProps> = ({
|
|
|
106
107
|
}, []);
|
|
107
108
|
|
|
108
109
|
return (
|
|
109
|
-
<
|
|
110
|
-
className={classes([
|
|
111
|
-
'dropin-modal',
|
|
112
|
-
['dropin-modal--dim', backgroundDim],
|
|
113
|
-
])}
|
|
114
|
-
>
|
|
110
|
+
<Portal>
|
|
115
111
|
<div
|
|
116
|
-
{...props}
|
|
117
112
|
className={classes([
|
|
118
|
-
'dropin-
|
|
119
|
-
[
|
|
120
|
-
className,
|
|
113
|
+
'dropin-modal',
|
|
114
|
+
['dropin-modal--dim', backgroundDim],
|
|
121
115
|
])}
|
|
122
116
|
>
|
|
123
117
|
<div
|
|
118
|
+
{...props}
|
|
124
119
|
className={classes([
|
|
125
|
-
'dropin-
|
|
126
|
-
[
|
|
120
|
+
'dropin-modal__body',
|
|
121
|
+
[`dropin-modal__body--${size}`, size],
|
|
122
|
+
className,
|
|
127
123
|
])}
|
|
128
124
|
>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
125
|
+
<div
|
|
126
|
+
className={classes([
|
|
127
|
+
'dropin-modal__header',
|
|
128
|
+
['dropin-modal__header-title', !!title],
|
|
129
|
+
])}
|
|
130
|
+
>
|
|
131
|
+
{title && (
|
|
132
|
+
<div className={classes(['dropin-modal__header-title-content'])}>
|
|
133
|
+
{title}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{showCloseButton && (
|
|
138
|
+
<Button
|
|
139
|
+
aria-label={translations.modalCloseLabel}
|
|
140
|
+
variant="tertiary"
|
|
141
|
+
className="dropin-modal__header-close-button"
|
|
142
|
+
onClick={handleOnClose}
|
|
143
|
+
icon={<CloseSVG />}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div
|
|
149
|
+
className={classes([
|
|
150
|
+
'dropin-modal__content',
|
|
151
|
+
['dropin-modal__body--centered', centered],
|
|
152
|
+
])}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</div>
|
|
153
156
|
</div>
|
|
154
157
|
</div>
|
|
155
|
-
</
|
|
158
|
+
</Portal>
|
|
156
159
|
);
|
|
157
160
|
};
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
import { Meta, StoryObj } from '@storybook/preact';
|
|
11
|
+
import { Portal } from './Portal';
|
|
12
|
+
import { Button } from '../Button';
|
|
13
|
+
import { useState } from 'preact/compat';
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Portal> = {
|
|
16
|
+
title: 'Components/Portal',
|
|
17
|
+
component: Portal,
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: 'centered',
|
|
20
|
+
},
|
|
21
|
+
tags: ['autodocs'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof Portal>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { Portal } from '@/elsie/components/Portal';
|
|
30
|
+
*
|
|
31
|
+
* <Portal>
|
|
32
|
+
* <div>👋 Howdy, I'm Howdy!</div>
|
|
33
|
+
* </Portal>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Portal with dynamic content
|
|
38
|
+
export const DynamicContent: Story = {
|
|
39
|
+
render: () => {
|
|
40
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
41
|
+
const [count, setCount] = useState(0);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div style={{ border: '2px dashed #ccc', padding: '20px' }}>
|
|
45
|
+
<Button onClick={() => setIsOpen(!isOpen)}>
|
|
46
|
+
{isOpen ? 'Close Portal' : 'Open Portal'}
|
|
47
|
+
</Button>
|
|
48
|
+
|
|
49
|
+
{isOpen && (
|
|
50
|
+
<Portal>
|
|
51
|
+
<div style={{
|
|
52
|
+
position: 'fixed',
|
|
53
|
+
top: '50%',
|
|
54
|
+
left: '50%',
|
|
55
|
+
transform: 'translate(-50%, -50%)',
|
|
56
|
+
background: 'white',
|
|
57
|
+
padding: '20px',
|
|
58
|
+
border: '1px solid #ccc',
|
|
59
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
60
|
+
}}>
|
|
61
|
+
<p>Portal content with counter: {count}</p>
|
|
62
|
+
<Button onClick={() => setCount(c => c + 1)}>
|
|
63
|
+
Increment Counter
|
|
64
|
+
</Button>
|
|
65
|
+
<Button
|
|
66
|
+
variant="tertiary"
|
|
67
|
+
onClick={() => setIsOpen(false)}
|
|
68
|
+
style={{ marginLeft: '8px' }}
|
|
69
|
+
>
|
|
70
|
+
Close
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</Portal>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Portal with nested portals
|
|
81
|
+
export const NestedPortals: Story = {
|
|
82
|
+
render: () => {
|
|
83
|
+
const [isOuterOpen, setOuterOpen] = useState(false);
|
|
84
|
+
const [isInnerOpen, setInnerOpen] = useState(false);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div style={{ border: '2px dashed #ccc', padding: '20px' }}>
|
|
88
|
+
<Button onClick={() => setOuterOpen(!isOuterOpen)}>
|
|
89
|
+
{isOuterOpen ? 'Close Outer Portal' : 'Open Outer Portal'}
|
|
90
|
+
</Button>
|
|
91
|
+
|
|
92
|
+
{isOuterOpen && (
|
|
93
|
+
<Portal>
|
|
94
|
+
<div style={{
|
|
95
|
+
position: 'fixed',
|
|
96
|
+
top: '50%',
|
|
97
|
+
left: '50%',
|
|
98
|
+
transform: 'translate(-50%, -50%)',
|
|
99
|
+
background: 'white',
|
|
100
|
+
padding: '20px',
|
|
101
|
+
border: '1px solid #ccc',
|
|
102
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
103
|
+
}}>
|
|
104
|
+
<p>Outer Portal Content</p>
|
|
105
|
+
<Button onClick={() => setInnerOpen(!isInnerOpen)}>
|
|
106
|
+
{isInnerOpen ? 'Close Inner Portal' : 'Open Inner Portal'}
|
|
107
|
+
</Button>
|
|
108
|
+
|
|
109
|
+
{isInnerOpen && (
|
|
110
|
+
<Portal>
|
|
111
|
+
<div style={{
|
|
112
|
+
position: 'fixed',
|
|
113
|
+
top: '60%',
|
|
114
|
+
left: '50%',
|
|
115
|
+
transform: 'translate(-50%, -50%)',
|
|
116
|
+
background: 'white',
|
|
117
|
+
padding: '20px',
|
|
118
|
+
border: '1px solid #ccc',
|
|
119
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
|
120
|
+
}}>
|
|
121
|
+
<p>Inner Portal Content</p>
|
|
122
|
+
<Button
|
|
123
|
+
variant="tertiary"
|
|
124
|
+
onClick={() => setInnerOpen(false)}
|
|
125
|
+
>
|
|
126
|
+
Close Inner Portal
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
</Portal>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
<Button
|
|
133
|
+
variant="tertiary"
|
|
134
|
+
onClick={() => setOuterOpen(false)}
|
|
135
|
+
style={{ marginLeft: '8px' }}
|
|
136
|
+
>
|
|
137
|
+
Close Outer Portal
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
</Portal>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import { ComponentChildren, render } from 'preact';
|
|
11
|
+
import { FunctionComponent, useEffect, useRef } from 'preact/compat';
|
|
12
|
+
|
|
13
|
+
interface PortalProps {
|
|
14
|
+
children: ComponentChildren;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Portal: FunctionComponent<PortalProps> = ({ children }) => {
|
|
18
|
+
const portalRoot = useRef<HTMLDivElement | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Create portal root if it doesn't exist
|
|
22
|
+
if (!portalRoot.current) {
|
|
23
|
+
portalRoot.current = document.createElement('div');
|
|
24
|
+
portalRoot.current.setAttribute('data-portal-root', '');
|
|
25
|
+
portalRoot.current.classList.add('dropin-design');
|
|
26
|
+
document.body.appendChild(portalRoot.current);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Render children into portal root
|
|
30
|
+
render(children, portalRoot.current);
|
|
31
|
+
|
|
32
|
+
// Cleanup
|
|
33
|
+
return () => {
|
|
34
|
+
if (portalRoot.current) {
|
|
35
|
+
render(null, portalRoot.current);
|
|
36
|
+
portalRoot.current.remove();
|
|
37
|
+
portalRoot.current = null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}, [children]);
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
|
package/src/icons/index.ts
CHANGED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { provider as UI, Image } from '@adobe-commerce/elsie/components';
|
|
2
|
+
import { getConfigValue } from '@adobe-commerce/elsie/lib/aem/configs';
|
|
3
|
+
|
|
4
|
+
interface AemAssetsParams {
|
|
5
|
+
quality?: number;
|
|
6
|
+
format?: string;
|
|
7
|
+
crop?: {
|
|
8
|
+
xOrigin?: number;
|
|
9
|
+
yOrigin?: number;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
};
|
|
13
|
+
size?: {
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AemAssetsImageConfig {
|
|
23
|
+
wrapper?: HTMLElement;
|
|
24
|
+
alias: string;
|
|
25
|
+
params: AemAssetsParams;
|
|
26
|
+
imageProps: {
|
|
27
|
+
src: string;
|
|
28
|
+
width?: number;
|
|
29
|
+
height?: number;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
};
|
|
32
|
+
src?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RenderContext {
|
|
36
|
+
replaceWith: (element: HTMLElement) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isAemAssetsEnabled(): boolean {
|
|
40
|
+
const config = getConfigValue('commerce-assets-enabled');
|
|
41
|
+
|
|
42
|
+
return config && (
|
|
43
|
+
(typeof config === 'string' && config.toLowerCase() === 'true')
|
|
44
|
+
|| (typeof config === 'boolean' && config === true)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getDefaultAemAssetsOptimizationParams(): { quality: number; format: string } {
|
|
49
|
+
// See: https://adobe-aem-assets-delivery-experimental.redoc.ly/
|
|
50
|
+
return {
|
|
51
|
+
quality: 80,
|
|
52
|
+
format: 'webp',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes the given URL to ensure it is a valid URL.
|
|
58
|
+
* @param {string} url - The URL to normalize.
|
|
59
|
+
* @returns {string} The normalized URL.
|
|
60
|
+
*/
|
|
61
|
+
function normalizeUrl(url: string): string {
|
|
62
|
+
let imageUrl = url;
|
|
63
|
+
|
|
64
|
+
if (imageUrl.startsWith('//')) {
|
|
65
|
+
// Use current window's protocol.
|
|
66
|
+
const { protocol } = window.location;
|
|
67
|
+
console.log('protocol', protocol);
|
|
68
|
+
imageUrl = protocol + imageUrl;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return imageUrl;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isAemAssetsUrl(url: string | URL): boolean {
|
|
75
|
+
const assetsUrl = typeof url === 'string' ? new URL(normalizeUrl(url)) : url;
|
|
76
|
+
|
|
77
|
+
if (!assetsUrl.pathname.startsWith('/adobe/assets/urn:aaid:aem')) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function generateAemAssetsOptimizedUrl(url: string, alias: string, params: AemAssetsParams = {}): string {
|
|
85
|
+
const defaultParams = getDefaultAemAssetsOptimizationParams();
|
|
86
|
+
const mergedParams: AemAssetsParams = { ...defaultParams, ...params };
|
|
87
|
+
|
|
88
|
+
// Destructure the ones that need special handling
|
|
89
|
+
const {
|
|
90
|
+
format,
|
|
91
|
+
crop,
|
|
92
|
+
size,
|
|
93
|
+
...optimizedParams
|
|
94
|
+
} = mergedParams;
|
|
95
|
+
|
|
96
|
+
const searchParams = new URLSearchParams(optimizedParams);
|
|
97
|
+
|
|
98
|
+
if (crop) {
|
|
99
|
+
const [xOrigin, yOrigin] = [crop.xOrigin || 0, crop.yOrigin || 0];
|
|
100
|
+
const [width, height] = [crop.width || 100, crop.height || 100];
|
|
101
|
+
|
|
102
|
+
const cropTransform = `${xOrigin}p,${yOrigin}p,${width}p,${height}p`;
|
|
103
|
+
searchParams.set('crop', cropTransform);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Both values must be present
|
|
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()}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function tryGenerateAemAssetsOptimizedUrl(url: string, alias: string, params: AemAssetsParams = {}): string {
|
|
115
|
+
const assetsEnabled = isAemAssetsEnabled();
|
|
116
|
+
|
|
117
|
+
if (!(assetsEnabled)) {
|
|
118
|
+
// No-op, doesn't do anything.
|
|
119
|
+
return url;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const assetsUrl = new URL(normalizeUrl(url));
|
|
123
|
+
|
|
124
|
+
if (!isAemAssetsUrl(assetsUrl)) {
|
|
125
|
+
// Not an AEM Assets URL, so no-op.
|
|
126
|
+
return url;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const base = assetsUrl.origin + assetsUrl.pathname;
|
|
130
|
+
return generateAemAssetsOptimizedUrl(base, alias, params);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function makeAemAssetsImageSlot(
|
|
134
|
+
config: AemAssetsImageConfig,
|
|
135
|
+
) {
|
|
136
|
+
return (ctx: RenderContext) => {
|
|
137
|
+
const {
|
|
138
|
+
wrapper,
|
|
139
|
+
alias,
|
|
140
|
+
params,
|
|
141
|
+
imageProps,
|
|
142
|
+
src,
|
|
143
|
+
} = config;
|
|
144
|
+
|
|
145
|
+
const container = wrapper ?? document.createElement('div');
|
|
146
|
+
const imageSrc = generateAemAssetsOptimizedUrl(src || imageProps.src, alias, params);
|
|
147
|
+
|
|
148
|
+
UI.render(Image as any, {
|
|
149
|
+
...imageProps,
|
|
150
|
+
|
|
151
|
+
src: imageSrc,
|
|
152
|
+
params: {
|
|
153
|
+
width: params.width,
|
|
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);
|
|
162
|
+
|
|
163
|
+
ctx.replaceWith(container);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function tryRenderAemAssetsImage(ctx: RenderContext, config: AemAssetsImageConfig): void {
|
|
168
|
+
// Renders an equivalent of the default image.
|
|
169
|
+
function renderDefaultImage(): void {
|
|
170
|
+
const container = config.wrapper ?? document.createElement('div');
|
|
171
|
+
const { imageProps } = config;
|
|
172
|
+
|
|
173
|
+
(UI.render as any)(Image, imageProps)(container);
|
|
174
|
+
ctx.replaceWith(container);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const assetsEnabled = isAemAssetsEnabled();
|
|
178
|
+
|
|
179
|
+
if (!(assetsEnabled)) {
|
|
180
|
+
// No-op, render the default image.
|
|
181
|
+
renderDefaultImage();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { imageProps, src, ...slotConfig } = config;
|
|
186
|
+
const assetsUrl = new URL(normalizeUrl(src ?? imageProps.src));
|
|
187
|
+
|
|
188
|
+
if (!isAemAssetsUrl(assetsUrl)) {
|
|
189
|
+
// Not an AEM Assets URL, so render the default image.
|
|
190
|
+
renderDefaultImage();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
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);
|
|
207
|
+
}
|