@builder.io/sdk-solid 0.0.15 → 0.0.17
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/README.md +5 -1
- package/package.json +17 -5
- package/solid-index.jsx +5 -0
- package/src/blocks/BaseText.jsx +9 -0
- package/src/blocks/button/button.jsx +9 -6
- package/src/blocks/columns/columns.jsx +54 -52
- package/src/blocks/columns/component-info.js +26 -1
- package/src/blocks/custom-code/custom-code.jsx +35 -38
- package/src/blocks/embed/component-info.js +21 -1
- package/src/blocks/embed/embed.jsx +37 -42
- package/src/blocks/embed/helpers.js +9 -0
- package/src/blocks/form/form.jsx +175 -176
- package/src/blocks/image/component-info.js +48 -1
- package/src/blocks/image/image.helpers.js +48 -0
- package/src/blocks/image/image.jsx +44 -13
- package/src/blocks/img/img.jsx +1 -1
- package/src/blocks/symbol/component-info.js +1 -0
- package/src/blocks/symbol/symbol.jsx +40 -13
- package/src/blocks/text/text.jsx +1 -1
- package/src/blocks/util.js +7 -0
- package/src/blocks/video/video.jsx +21 -2
- package/src/components/render-block/block-styles.jsx +41 -31
- package/src/components/render-block/render-block.helpers.js +23 -0
- package/src/components/render-block/render-block.jsx +205 -90
- package/src/components/render-block/render-component-with-context.jsx +36 -0
- package/src/components/render-block/render-component.jsx +28 -0
- package/src/components/render-block/render-repeated-block.jsx +36 -0
- package/src/components/render-block/types.js +0 -0
- package/src/components/render-blocks.jsx +42 -33
- package/src/components/render-content/components/render-styles.jsx +39 -42
- package/src/components/render-content/index.js +4 -0
- package/src/components/render-content/render-content.jsx +200 -129
- package/src/components/render-inlined-styles.jsx +21 -5
- package/src/constants/builder-registered-components.js +54 -0
- package/src/constants/device-sizes.js +3 -21
- package/src/context/builder.context.js +3 -1
- package/src/context/types.js +0 -0
- package/src/functions/camel-to-kebab-case.js +4 -0
- package/src/functions/convert-style-object.js +14 -0
- package/src/functions/evaluate.js +1 -1
- package/src/functions/extract-text-styles.js +22 -0
- package/src/functions/fast-clone.js +4 -0
- package/src/functions/get-block-actions-handler.js +12 -0
- package/src/functions/get-block-actions.js +2 -7
- package/src/functions/get-block-component-options.js +6 -1
- package/src/functions/get-block-styles.js +27 -19
- package/src/functions/get-builder-search-params/index.js +17 -2
- package/src/functions/get-content/ab-testing.js +99 -0
- package/src/functions/get-content/fn.test.js +1 -1
- package/src/functions/get-content/index.js +20 -62
- package/src/functions/get-content/types.js +0 -0
- package/src/functions/get-fetch.js +2 -2
- package/src/functions/get-processed-block.js +24 -11
- package/src/functions/get-processed-block.test.js +2 -1
- package/src/functions/mark-mutable.js +10 -0
- package/src/functions/register-component.js +45 -26
- package/src/functions/sanitize-styles.js +4 -0
- package/src/functions/track.js +108 -13
- package/src/helpers/ab-tests.js +16 -0
- package/src/helpers/cookie.js +79 -0
- package/src/helpers/css.js +28 -0
- package/src/helpers/flatten.js +34 -0
- package/src/helpers/localStorage.js +34 -0
- package/src/helpers/nullable.js +4 -0
- package/src/helpers/sessionId.js +49 -0
- package/src/helpers/time.js +5 -0
- package/src/helpers/url.js +10 -0
- package/src/helpers/url.test.js +15 -0
- package/src/helpers/uuid.js +13 -0
- package/src/helpers/visitorId.js +33 -0
- package/src/index-helpers/blocks-exports.js +11 -9
- package/src/scripts/init-editing.js +4 -5
- package/src/types/can-track.js +0 -0
- package/src/types/components.js +0 -0
- package/src/types/element.js +0 -0
- package/src/blocks/button/button.lite.tsx +0 -20
- package/src/blocks/button/index.js +0 -7
- package/src/blocks/columns/columns.lite.tsx +0 -102
- package/src/blocks/columns/index.js +0 -7
- package/src/blocks/custom-code/custom-code.lite.tsx +0 -67
- package/src/blocks/custom-code/index.js +0 -7
- package/src/blocks/embed/embed.lite.tsx +0 -59
- package/src/blocks/embed/index.js +0 -7
- package/src/blocks/form/form.lite.tsx +0 -293
- package/src/blocks/form/index.js +0 -7
- package/src/blocks/fragment/fragment.lite.tsx +0 -5
- package/src/blocks/fragment/index.js +0 -7
- package/src/blocks/image/image.lite.tsx +0 -83
- package/src/blocks/image/index.js +0 -7
- package/src/blocks/img/img.lite.tsx +0 -18
- package/src/blocks/img/index.js +0 -7
- package/src/blocks/input/index.js +0 -7
- package/src/blocks/input/input.lite.tsx +0 -20
- package/src/blocks/raw-text/index.js +0 -7
- package/src/blocks/raw-text/raw-text.lite.tsx +0 -10
- package/src/blocks/section/index.js +0 -7
- package/src/blocks/section/section.lite.tsx +0 -18
- package/src/blocks/select/index.js +0 -7
- package/src/blocks/select/select.lite.tsx +0 -28
- package/src/blocks/submit-button/index.js +0 -7
- package/src/blocks/submit-button/submit-button.lite.tsx +0 -9
- package/src/blocks/symbol/index.js +0 -7
- package/src/blocks/symbol/symbol.lite.tsx +0 -39
- package/src/blocks/text/index.js +0 -7
- package/src/blocks/text/text.lite.tsx +0 -5
- package/src/blocks/textarea/index.js +0 -7
- package/src/blocks/textarea/textarea.lite.tsx +0 -13
- package/src/blocks/video/index.js +0 -7
- package/src/blocks/video/video.lite.tsx +0 -26
- package/src/components/error-boundary.jsx +0 -5
- package/src/components/error-boundary.lite.tsx +0 -5
- package/src/components/render-block/block-styles.lite.tsx +0 -38
- package/src/components/render-block/render-block.lite.tsx +0 -119
- package/src/components/render-blocks.lite.tsx +0 -75
- package/src/components/render-content/components/render-styles.lite.tsx +0 -76
- package/src/components/render-content/render-content.lite.tsx +0 -232
- package/src/components/render-inlined-styles.lite.tsx +0 -5
- package/src/functions/macro-eval.js +0 -5
- package/src/functions/previewing-model-name.js +0 -11
package/src/blocks/form/form.jsx
CHANGED
|
@@ -1,250 +1,249 @@
|
|
|
1
|
-
import { Show, For } from "solid-js";
|
|
2
|
-
import { createMutable } from "solid-js/store";
|
|
1
|
+
import { useContext, Show, For, createSignal } from "solid-js";
|
|
3
2
|
import { css } from "solid-styled-components";
|
|
4
|
-
import RenderBlock from "../../components/render-block/render-block
|
|
3
|
+
import RenderBlock from "../../components/render-block/render-block.jsx";
|
|
4
|
+
import BuilderBlocks from "../../components/render-blocks.jsx";
|
|
5
5
|
import { isEditing } from "../../functions/is-editing.js";
|
|
6
6
|
|
|
7
7
|
function FormComponent(props) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
formErrorMessage: "",
|
|
8
|
+
const [formState, setFormState] = createSignal("unsubmitted");
|
|
9
|
+
const [responseData, setResponseData] = createSignal(null);
|
|
10
|
+
const [formErrorMessage, setFormErrorMessage] = createSignal("");
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
function submissionState() {
|
|
13
|
+
return isEditing() && props.previewState || formState();
|
|
14
|
+
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
function onSubmit(event) {
|
|
17
|
+
const sendWithJs = props.sendWithJs || props.sendSubmissionsTo === "email";
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
if (props.sendSubmissionsTo === "zapier") {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
} else if (sendWithJs) {
|
|
22
|
+
if (!(props.action || props.sendSubmissionsTo === "email")) {
|
|
21
23
|
event.preventDefault();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
event.preventDefault();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
const el = event.currentTarget;
|
|
29
|
+
const headers = props.customHeaders || {};
|
|
30
|
+
let body;
|
|
31
|
+
const formData = new FormData(el); // TODO: maybe support null
|
|
32
|
+
|
|
33
|
+
const formPairs = Array.from(event.currentTarget.querySelectorAll("input,select,textarea")).filter(el => !!el.name).map(el => {
|
|
34
|
+
let value;
|
|
35
|
+
const key = el.name;
|
|
36
|
+
|
|
37
|
+
if (el instanceof HTMLInputElement) {
|
|
38
|
+
if (el.type === "radio") {
|
|
39
|
+
if (el.checked) {
|
|
40
|
+
value = el.name;
|
|
41
|
+
return {
|
|
42
|
+
key,
|
|
43
|
+
value
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
} else if (el.type === "checkbox") {
|
|
47
|
+
value = el.checked;
|
|
48
|
+
} else if (el.type === "number" || el.type === "range") {
|
|
49
|
+
const num = el.valueAsNumber;
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
} else if (el.type === "file") {
|
|
56
|
-
// TODO: one vs multiple files
|
|
57
|
-
value = el.files;
|
|
58
|
-
} else {
|
|
59
|
-
value = el.value;
|
|
51
|
+
if (!isNaN(num)) {
|
|
52
|
+
value = num;
|
|
60
53
|
}
|
|
54
|
+
} else if (el.type === "file") {
|
|
55
|
+
// TODO: one vs multiple files
|
|
56
|
+
value = el.files;
|
|
61
57
|
} else {
|
|
62
58
|
value = el.value;
|
|
63
59
|
}
|
|
60
|
+
} else {
|
|
61
|
+
value = el.value;
|
|
62
|
+
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
return {
|
|
65
|
+
key,
|
|
66
|
+
value
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
let contentType = props.contentType;
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
if (props.sendSubmissionsTo === "email") {
|
|
72
|
+
contentType = "multipart/form-data";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Array.from(formPairs).forEach(({
|
|
76
|
+
value
|
|
77
|
+
}) => {
|
|
78
|
+
if (value instanceof File || Array.isArray(value) && value[0] instanceof File || value instanceof FileList) {
|
|
73
79
|
contentType = "multipart/form-data";
|
|
74
80
|
}
|
|
75
|
-
|
|
81
|
+
}); // TODO: send as urlEncoded or multipart by default
|
|
82
|
+
// because of ease of use and reliability in browser API
|
|
83
|
+
// for encoding the form?
|
|
84
|
+
|
|
85
|
+
if (contentType !== "application/json") {
|
|
86
|
+
body = formData;
|
|
87
|
+
} else {
|
|
88
|
+
// Json
|
|
89
|
+
const json = {};
|
|
76
90
|
Array.from(formPairs).forEach(({
|
|
77
|
-
value
|
|
91
|
+
value,
|
|
92
|
+
key
|
|
78
93
|
}) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// because of ease of use and reliability in browser API
|
|
84
|
-
// for encoding the form?
|
|
94
|
+
set(json, key, value);
|
|
95
|
+
});
|
|
96
|
+
body = JSON.stringify(json);
|
|
97
|
+
}
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Array.from(formPairs).forEach(({
|
|
92
|
-
value,
|
|
93
|
-
key
|
|
94
|
-
}) => {
|
|
95
|
-
set(json, key, value);
|
|
96
|
-
});
|
|
97
|
-
body = JSON.stringify(json);
|
|
99
|
+
if (contentType && contentType !== "multipart/form-data") {
|
|
100
|
+
if (
|
|
101
|
+
/* Zapier doesn't allow content-type header to be sent from browsers */
|
|
102
|
+
!(sendWithJs && props.action?.includes("zapier.com"))) {
|
|
103
|
+
headers["content-type"] = contentType;
|
|
98
104
|
}
|
|
105
|
+
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
!(sendWithJs && props.action?.includes("zapier.com"))) {
|
|
104
|
-
headers["content-type"] = contentType;
|
|
105
|
-
}
|
|
107
|
+
const presubmitEvent = new CustomEvent("presubmit", {
|
|
108
|
+
detail: {
|
|
109
|
+
body
|
|
106
110
|
}
|
|
111
|
+
});
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
body
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (formRef) {
|
|
115
|
-
formRef.dispatchEvent(presubmitEvent);
|
|
113
|
+
if (formRef) {
|
|
114
|
+
formRef.dispatchEvent(presubmitEvent);
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
116
|
+
if (presubmitEvent.defaultPrevented) {
|
|
117
|
+
return;
|
|
120
118
|
}
|
|
119
|
+
}
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (contentType && contentType.indexOf("application/json") !== -1) {
|
|
135
|
-
body = await res.json();
|
|
136
|
-
} else {
|
|
137
|
-
body = await res.text();
|
|
138
|
-
}
|
|
121
|
+
setFormState("sending");
|
|
122
|
+
const formUrl = `${builder.env === "dev" ? "http://localhost:5000" : "https://builder.io"}/api/v1/form-submit?apiKey=${builder.apiKey}&to=${btoa(props.sendSubmissionsToEmail || "")}&name=${encodeURIComponent(props.name || "")}`;
|
|
123
|
+
fetch(props.sendSubmissionsTo === "email" ? formUrl : props.action,
|
|
124
|
+
/* TODO: throw error if no action URL */
|
|
125
|
+
{
|
|
126
|
+
body,
|
|
127
|
+
headers,
|
|
128
|
+
method: props.method || "post"
|
|
129
|
+
}).then(async res => {
|
|
130
|
+
let body;
|
|
131
|
+
const contentType = res.headers.get("content-type");
|
|
139
132
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
if (contentType && contentType.indexOf("application/json") !== -1) {
|
|
134
|
+
body = await res.json();
|
|
135
|
+
} else {
|
|
136
|
+
body = await res.text();
|
|
137
|
+
}
|
|
143
138
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
error: - email has been taken */
|
|
148
|
-
message = JSON.stringify(message);
|
|
149
|
-
}
|
|
139
|
+
if (!res.ok && props.errorMessagePath) {
|
|
140
|
+
/* TODO: allow supplying an error formatter function */
|
|
141
|
+
let message = get(body, props.errorMessagePath);
|
|
150
142
|
|
|
151
|
-
|
|
143
|
+
if (message) {
|
|
144
|
+
if (typeof message !== "string") {
|
|
145
|
+
/* TODO: ideally convert json to yaml so it woul dbe like
|
|
146
|
+
error: - email has been taken */
|
|
147
|
+
message = JSON.stringify(message);
|
|
152
148
|
}
|
|
149
|
+
|
|
150
|
+
setFormErrorMessage(message);
|
|
153
151
|
}
|
|
152
|
+
}
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
setResponseData(body);
|
|
155
|
+
setFormState(res.ok ? "success" : "error");
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
if (res.ok) {
|
|
158
|
+
const submitSuccessEvent = new CustomEvent("submit:success", {
|
|
159
|
+
detail: {
|
|
160
|
+
res,
|
|
161
|
+
body
|
|
162
|
+
}
|
|
163
|
+
});
|
|
165
164
|
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
if (formRef) {
|
|
166
|
+
formRef.dispatchEvent(submitSuccessEvent);
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
if (submitSuccessEvent.defaultPrevented) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
/* TODO: option to turn this on/off? */
|
|
173
172
|
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
174
|
+
if (props.resetFormOnSubmit !== false) {
|
|
175
|
+
formRef.reset();
|
|
178
176
|
}
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
}
|
|
178
|
+
/* TODO: client side route event first that can be preventDefaulted */
|
|
181
179
|
|
|
182
|
-
if (props.successUrl) {
|
|
183
|
-
if (formRef) {
|
|
184
|
-
const event = new CustomEvent("route", {
|
|
185
|
-
detail: {
|
|
186
|
-
url: props.successUrl
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
formRef.dispatchEvent(event);
|
|
190
180
|
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
if (props.successUrl) {
|
|
182
|
+
if (formRef) {
|
|
183
|
+
const event = new CustomEvent("route", {
|
|
184
|
+
detail: {
|
|
185
|
+
url: props.successUrl
|
|
193
186
|
}
|
|
194
|
-
}
|
|
187
|
+
});
|
|
188
|
+
formRef.dispatchEvent(event);
|
|
189
|
+
|
|
190
|
+
if (!event.defaultPrevented) {
|
|
195
191
|
location.href = props.successUrl;
|
|
196
192
|
}
|
|
193
|
+
} else {
|
|
194
|
+
location.href = props.successUrl;
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
197
|
+
}
|
|
198
|
+
}, err => {
|
|
199
|
+
const submitErrorEvent = new CustomEvent("submit:error", {
|
|
200
|
+
detail: {
|
|
201
|
+
error: err
|
|
202
|
+
}
|
|
203
|
+
});
|
|
205
204
|
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
if (formRef) {
|
|
206
|
+
formRef.dispatchEvent(submitErrorEvent);
|
|
208
207
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
208
|
+
if (submitErrorEvent.defaultPrevented) {
|
|
209
|
+
return;
|
|
212
210
|
}
|
|
211
|
+
}
|
|
213
212
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
213
|
+
setResponseData(err);
|
|
214
|
+
setFormState("error");
|
|
215
|
+
});
|
|
218
216
|
}
|
|
217
|
+
}
|
|
219
218
|
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
return <form {...props.attributes} validate={props.validate} ref={formRef} action={!props.sendWithJs && props.action} method={props.method} name={props.name} onSubmit={event =>
|
|
219
|
+
let formRef;
|
|
220
|
+
const builderContext = useContext(BuilderContext);
|
|
221
|
+
return <form {...props.attributes} validate={props.validate} ref={formRef} action={!props.sendWithJs && props.action} method={props.method} name={props.name} onSubmit={event => onSubmit(event)}>
|
|
223
222
|
<Show when={props.builderBlock && props.builderBlock.children}>
|
|
224
223
|
<For each={props.builderBlock?.children}>
|
|
225
224
|
{(block, _index) => {
|
|
226
225
|
const index = _index();
|
|
227
226
|
|
|
228
|
-
return <RenderBlock block={block}></RenderBlock>;
|
|
227
|
+
return <RenderBlock block={block} context={builderContext}></RenderBlock>;
|
|
229
228
|
}}
|
|
230
229
|
</For>
|
|
231
230
|
</Show>
|
|
232
|
-
<Show when={
|
|
231
|
+
<Show when={submissionState() === "error"}>
|
|
233
232
|
<BuilderBlocks dataPath="errorMessage" blocks={props.errorMessage}></BuilderBlocks>
|
|
234
233
|
</Show>
|
|
235
|
-
<Show when={
|
|
234
|
+
<Show when={submissionState() === "sending"}>
|
|
236
235
|
<BuilderBlocks dataPath="sendingMessage" blocks={props.sendingMessage}></BuilderBlocks>
|
|
237
236
|
</Show>
|
|
238
|
-
<Show when={
|
|
239
|
-
<pre class={css({
|
|
237
|
+
<Show when={submissionState() === "error" && responseData()}>
|
|
238
|
+
<pre class={"builder-form-error-text " + css({
|
|
240
239
|
padding: "10px",
|
|
241
240
|
color: "red",
|
|
242
241
|
textAlign: "center"
|
|
243
242
|
})}>
|
|
244
|
-
{JSON.stringify(
|
|
243
|
+
{JSON.stringify(responseData(), null, 2)}
|
|
245
244
|
</pre>
|
|
246
245
|
</Show>
|
|
247
|
-
<Show when={
|
|
246
|
+
<Show when={submissionState() === "success"}>
|
|
248
247
|
<BuilderBlocks dataPath="successMessage" blocks={props.successMessage}></BuilderBlocks>
|
|
249
248
|
</Show>
|
|
250
249
|
</form>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markSerializable } from "../util.js";
|
|
1
2
|
const componentInfo = {
|
|
2
3
|
name: "Image",
|
|
3
4
|
static: true,
|
|
@@ -18,7 +19,53 @@ const componentInfo = {
|
|
|
18
19
|
allowedFileTypes: ["jpeg", "jpg", "png", "svg"],
|
|
19
20
|
required: true,
|
|
20
21
|
defaultValue: "https://cdn.builder.io/api/v1/image/assets%2Fpwgjf0RoYWbdnJSbpBAjXNRMe9F2%2Ffb27a7c790324294af8be1c35fe30f4d",
|
|
21
|
-
onChange:
|
|
22
|
+
onChange: markSerializable((options) => {
|
|
23
|
+
const DEFAULT_ASPECT_RATIO = 0.7041;
|
|
24
|
+
options.delete("srcset");
|
|
25
|
+
options.delete("noWebp");
|
|
26
|
+
function loadImage(url, timeout = 6e4) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const img = document.createElement("img");
|
|
29
|
+
let loaded = false;
|
|
30
|
+
img.onload = () => {
|
|
31
|
+
loaded = true;
|
|
32
|
+
resolve(img);
|
|
33
|
+
};
|
|
34
|
+
img.addEventListener("error", (event) => {
|
|
35
|
+
console.warn("Image load failed", event.error);
|
|
36
|
+
reject(event.error);
|
|
37
|
+
});
|
|
38
|
+
img.src = url;
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (!loaded) {
|
|
41
|
+
reject(new Error("Image load timed out"));
|
|
42
|
+
}
|
|
43
|
+
}, timeout);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function round(num) {
|
|
47
|
+
return Math.round(num * 1e3) / 1e3;
|
|
48
|
+
}
|
|
49
|
+
const value = options.get("image");
|
|
50
|
+
const aspectRatio = options.get("aspectRatio");
|
|
51
|
+
fetch(value).then((res) => res.blob()).then((blob) => {
|
|
52
|
+
if (blob.type.includes("svg")) {
|
|
53
|
+
options.set("noWebp", true);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (value && (!aspectRatio || aspectRatio === DEFAULT_ASPECT_RATIO)) {
|
|
57
|
+
return loadImage(value).then((img) => {
|
|
58
|
+
const possiblyUpdatedAspectRatio = options.get("aspectRatio");
|
|
59
|
+
if (options.get("image") === value && (!possiblyUpdatedAspectRatio || possiblyUpdatedAspectRatio === DEFAULT_ASPECT_RATIO)) {
|
|
60
|
+
if (img.width && img.height) {
|
|
61
|
+
options.set("aspectRatio", round(img.height / img.width));
|
|
62
|
+
options.set("height", img.height);
|
|
63
|
+
options.set("width", img.width);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
})
|
|
22
69
|
},
|
|
23
70
|
{
|
|
24
71
|
name: "backgroundSize",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function removeProtocol(path) {
|
|
2
|
+
return path.replace(/http(s)?:/, "");
|
|
3
|
+
}
|
|
4
|
+
function updateQueryParam(uri = "", key, value) {
|
|
5
|
+
const re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
|
|
6
|
+
const separator = uri.indexOf("?") !== -1 ? "&" : "?";
|
|
7
|
+
if (uri.match(re)) {
|
|
8
|
+
return uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2");
|
|
9
|
+
}
|
|
10
|
+
return uri + separator + key + "=" + encodeURIComponent(value);
|
|
11
|
+
}
|
|
12
|
+
function getShopifyImageUrl(src, size) {
|
|
13
|
+
if (!src || !(src == null ? void 0 : src.match(/cdn\.shopify\.com/)) || !size) {
|
|
14
|
+
return src;
|
|
15
|
+
}
|
|
16
|
+
if (size === "master") {
|
|
17
|
+
return removeProtocol(src);
|
|
18
|
+
}
|
|
19
|
+
const match = src.match(/(_\d+x(\d+)?)?(\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?)/i);
|
|
20
|
+
if (match) {
|
|
21
|
+
const prefix = src.split(match[0]);
|
|
22
|
+
const suffix = match[3];
|
|
23
|
+
const useSize = size.match("x") ? size : `${size}x`;
|
|
24
|
+
return removeProtocol(`${prefix[0]}_${useSize}${suffix}`);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function getSrcSet(url) {
|
|
29
|
+
if (!url) {
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
const sizes = [100, 200, 400, 800, 1200, 1600, 2e3];
|
|
33
|
+
if (url.match(/builder\.io/)) {
|
|
34
|
+
let srcUrl = url;
|
|
35
|
+
const widthInSrc = Number(url.split("?width=")[1]);
|
|
36
|
+
if (!isNaN(widthInSrc)) {
|
|
37
|
+
srcUrl = `${srcUrl} ${widthInSrc}w`;
|
|
38
|
+
}
|
|
39
|
+
return sizes.filter((size) => size !== widthInSrc).map((size) => `${updateQueryParam(url, "width", size)} ${size}w`).concat([srcUrl]).join(", ");
|
|
40
|
+
}
|
|
41
|
+
if (url.match(/cdn\.shopify\.com/)) {
|
|
42
|
+
return sizes.map((size) => [getShopifyImageUrl(url, `${size}x${size}`), size]).filter(([sizeUrl]) => !!sizeUrl).map(([sizeUrl, size]) => `${sizeUrl} ${size}w`).concat([url]).join(", ");
|
|
43
|
+
}
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
getSrcSet
|
|
48
|
+
};
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import { Show } from "solid-js";
|
|
2
2
|
import { css } from "solid-styled-components";
|
|
3
|
+
import { getSrcSet } from "./image.helpers.js";
|
|
3
4
|
|
|
4
5
|
function Image(props) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
function srcSetToUse() {
|
|
7
|
+
const imageToUse = props.image || props.src;
|
|
8
|
+
const url = imageToUse;
|
|
9
|
+
|
|
10
|
+
if (!url || // We can auto add srcset for cdn.builder.io and shopify
|
|
11
|
+
// images, otherwise you can supply this prop manually
|
|
12
|
+
!(url.match(/builder\.io/) || url.match(/cdn\.shopify\.com/))) {
|
|
13
|
+
return props.srcset;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (props.srcset && props.image?.includes("builder.io/api/v1/image")) {
|
|
17
|
+
if (!props.srcset.includes(props.image.split("?")[0])) {
|
|
18
|
+
console.debug("Removed given srcset");
|
|
19
|
+
return getSrcSet(url);
|
|
20
|
+
}
|
|
21
|
+
} else if (props.image && !props.srcset) {
|
|
22
|
+
return getSrcSet(url);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return getSrcSet(url);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function webpSrcSet() {
|
|
29
|
+
if (srcSetToUse()?.match(/builder\.io/) && !props.noWebp) {
|
|
30
|
+
return srcSetToUse().replace(/\?/g, "?format=webp&");
|
|
31
|
+
} else {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return <>
|
|
8
37
|
<picture>
|
|
38
|
+
<Show when={webpSrcSet()}>
|
|
39
|
+
<source type="image/webp" srcset={webpSrcSet()} />
|
|
40
|
+
</Show>
|
|
9
41
|
<img class={"builder-image" + (props.className ? " " + props.className : "") + " " + css({
|
|
10
42
|
opacity: "1",
|
|
11
43
|
transition: "opacity 0.2s ease-in-out",
|
|
@@ -14,27 +46,26 @@ function Image(props) {
|
|
|
14
46
|
width: "100%",
|
|
15
47
|
top: "0px",
|
|
16
48
|
left: "0px"
|
|
17
|
-
})} loading="lazy" alt={props.altText}
|
|
49
|
+
})} loading="lazy" alt={props.altText} role={props.altText ? "presentation" : undefined} style={{
|
|
18
50
|
"object-position": props.backgroundSize || "center",
|
|
19
51
|
"object-fit": props.backgroundSize || "cover"
|
|
20
|
-
}} src={props.image} srcset={
|
|
21
|
-
<source
|
|
52
|
+
}} src={props.image} srcset={srcSetToUse()} sizes={props.sizes} />
|
|
53
|
+
<source srcset={srcSetToUse()} />
|
|
22
54
|
</picture>
|
|
23
|
-
<Show when={props.aspectRatio && !(props.
|
|
24
|
-
<div class={css({
|
|
55
|
+
<Show when={props.aspectRatio && !(props.builderBlock?.children?.length && props.fitContent)}>
|
|
56
|
+
<div class={"builder-image-sizer " + css({
|
|
25
57
|
width: "100%",
|
|
26
58
|
pointerEvents: "none",
|
|
27
59
|
fontSize: "0"
|
|
28
60
|
})} style={{
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
29
62
|
"padding-top": props.aspectRatio * 100 + "%"
|
|
30
|
-
}}>
|
|
31
|
-
{" "}
|
|
32
|
-
</div>
|
|
63
|
+
}}></div>
|
|
33
64
|
</Show>
|
|
34
65
|
<Show when={props.builderBlock?.children?.length && props.fitContent}>
|
|
35
66
|
{props.children}
|
|
36
67
|
</Show>
|
|
37
|
-
<Show when={!props.fitContent}>
|
|
68
|
+
<Show when={!props.fitContent && props.children}>
|
|
38
69
|
<div class={css({
|
|
39
70
|
display: "flex",
|
|
40
71
|
flexDirection: "column",
|
|
@@ -48,7 +79,7 @@ function Image(props) {
|
|
|
48
79
|
{props.children}
|
|
49
80
|
</div>
|
|
50
81
|
</Show>
|
|
51
|
-
|
|
82
|
+
</>;
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
export default Image;
|
package/src/blocks/img/img.jsx
CHANGED
|
@@ -4,7 +4,7 @@ function ImgComponent(props) {
|
|
|
4
4
|
return <img {...props.attributes} style={{
|
|
5
5
|
"object-fit": props.backgroundSize || "cover",
|
|
6
6
|
"object-position": props.backgroundPosition || "center"
|
|
7
|
-
}} key={isEditing() && props.imgSrc || "default-key"} alt={props.altText} src={props.imgSrc} />;
|
|
7
|
+
}} key={isEditing() && props.imgSrc || "default-key"} alt={props.altText} src={props.imgSrc || props.image} />;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export default ImgComponent;
|