@builder.io/sdk-solid 0.0.16 → 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.
Files changed (119) hide show
  1. package/README.md +5 -1
  2. package/package.json +17 -5
  3. package/solid-index.jsx +5 -0
  4. package/src/blocks/BaseText.jsx +9 -0
  5. package/src/blocks/button/button.jsx +9 -6
  6. package/src/blocks/columns/columns.jsx +54 -52
  7. package/src/blocks/columns/component-info.js +26 -1
  8. package/src/blocks/custom-code/custom-code.jsx +35 -38
  9. package/src/blocks/embed/component-info.js +21 -1
  10. package/src/blocks/embed/embed.jsx +37 -42
  11. package/src/blocks/embed/helpers.js +9 -0
  12. package/src/blocks/form/form.jsx +175 -176
  13. package/src/blocks/image/component-info.js +48 -1
  14. package/src/blocks/image/image.helpers.js +48 -0
  15. package/src/blocks/image/image.jsx +44 -13
  16. package/src/blocks/img/img.jsx +1 -1
  17. package/src/blocks/symbol/component-info.js +1 -0
  18. package/src/blocks/symbol/symbol.jsx +39 -12
  19. package/src/blocks/text/text.jsx +1 -1
  20. package/src/blocks/util.js +7 -0
  21. package/src/blocks/video/video.jsx +21 -2
  22. package/src/components/render-block/block-styles.jsx +41 -31
  23. package/src/components/render-block/render-block.helpers.js +23 -0
  24. package/src/components/render-block/render-block.jsx +205 -90
  25. package/src/components/render-block/render-component-with-context.jsx +36 -0
  26. package/src/components/render-block/render-component.jsx +28 -0
  27. package/src/components/render-block/render-repeated-block.jsx +36 -0
  28. package/src/components/render-block/types.js +0 -0
  29. package/src/components/render-blocks.jsx +42 -33
  30. package/src/components/render-content/components/render-styles.jsx +39 -42
  31. package/src/components/render-content/index.js +4 -0
  32. package/src/components/render-content/render-content.jsx +199 -128
  33. package/src/components/render-inlined-styles.jsx +21 -5
  34. package/src/constants/builder-registered-components.js +54 -0
  35. package/src/constants/device-sizes.js +3 -21
  36. package/src/context/builder.context.js +3 -1
  37. package/src/context/types.js +0 -0
  38. package/src/functions/camel-to-kebab-case.js +4 -0
  39. package/src/functions/convert-style-object.js +14 -0
  40. package/src/functions/evaluate.js +1 -1
  41. package/src/functions/extract-text-styles.js +22 -0
  42. package/src/functions/fast-clone.js +4 -0
  43. package/src/functions/get-block-actions-handler.js +12 -0
  44. package/src/functions/get-block-actions.js +2 -7
  45. package/src/functions/get-block-component-options.js +6 -1
  46. package/src/functions/get-block-styles.js +27 -19
  47. package/src/functions/get-builder-search-params/index.js +17 -2
  48. package/src/functions/get-content/ab-testing.js +99 -0
  49. package/src/functions/get-content/fn.test.js +1 -1
  50. package/src/functions/get-content/index.js +20 -62
  51. package/src/functions/get-content/types.js +0 -0
  52. package/src/functions/get-fetch.js +2 -2
  53. package/src/functions/get-processed-block.js +24 -11
  54. package/src/functions/get-processed-block.test.js +2 -1
  55. package/src/functions/mark-mutable.js +10 -0
  56. package/src/functions/register-component.js +45 -26
  57. package/src/functions/sanitize-styles.js +4 -0
  58. package/src/functions/track.js +108 -13
  59. package/src/helpers/ab-tests.js +16 -0
  60. package/src/helpers/cookie.js +79 -0
  61. package/src/helpers/css.js +28 -0
  62. package/src/helpers/flatten.js +34 -0
  63. package/src/helpers/localStorage.js +34 -0
  64. package/src/helpers/nullable.js +4 -0
  65. package/src/helpers/sessionId.js +49 -0
  66. package/src/helpers/time.js +5 -0
  67. package/src/helpers/url.js +10 -0
  68. package/src/helpers/url.test.js +15 -0
  69. package/src/helpers/uuid.js +13 -0
  70. package/src/helpers/visitorId.js +33 -0
  71. package/src/index-helpers/blocks-exports.js +11 -9
  72. package/src/scripts/init-editing.js +4 -5
  73. package/src/types/can-track.js +0 -0
  74. package/src/types/components.js +0 -0
  75. package/src/types/element.js +0 -0
  76. package/src/blocks/button/button.lite.tsx +0 -20
  77. package/src/blocks/button/index.js +0 -7
  78. package/src/blocks/columns/columns.lite.tsx +0 -102
  79. package/src/blocks/columns/index.js +0 -7
  80. package/src/blocks/custom-code/custom-code.lite.tsx +0 -67
  81. package/src/blocks/custom-code/index.js +0 -7
  82. package/src/blocks/embed/embed.lite.tsx +0 -59
  83. package/src/blocks/embed/index.js +0 -7
  84. package/src/blocks/form/form.lite.tsx +0 -293
  85. package/src/blocks/form/index.js +0 -7
  86. package/src/blocks/fragment/fragment.lite.tsx +0 -5
  87. package/src/blocks/fragment/index.js +0 -7
  88. package/src/blocks/image/image.lite.tsx +0 -83
  89. package/src/blocks/image/index.js +0 -7
  90. package/src/blocks/img/img.lite.tsx +0 -18
  91. package/src/blocks/img/index.js +0 -7
  92. package/src/blocks/input/index.js +0 -7
  93. package/src/blocks/input/input.lite.tsx +0 -20
  94. package/src/blocks/raw-text/index.js +0 -7
  95. package/src/blocks/raw-text/raw-text.lite.tsx +0 -10
  96. package/src/blocks/section/index.js +0 -7
  97. package/src/blocks/section/section.lite.tsx +0 -18
  98. package/src/blocks/select/index.js +0 -7
  99. package/src/blocks/select/select.lite.tsx +0 -28
  100. package/src/blocks/submit-button/index.js +0 -7
  101. package/src/blocks/submit-button/submit-button.lite.tsx +0 -9
  102. package/src/blocks/symbol/index.js +0 -7
  103. package/src/blocks/symbol/symbol.lite.tsx +0 -39
  104. package/src/blocks/text/index.js +0 -7
  105. package/src/blocks/text/text.lite.tsx +0 -5
  106. package/src/blocks/textarea/index.js +0 -7
  107. package/src/blocks/textarea/textarea.lite.tsx +0 -13
  108. package/src/blocks/video/index.js +0 -7
  109. package/src/blocks/video/video.lite.tsx +0 -26
  110. package/src/components/error-boundary.jsx +0 -5
  111. package/src/components/error-boundary.lite.tsx +0 -5
  112. package/src/components/render-block/block-styles.lite.tsx +0 -38
  113. package/src/components/render-block/render-block.lite.tsx +0 -119
  114. package/src/components/render-blocks.lite.tsx +0 -75
  115. package/src/components/render-content/components/render-styles.lite.tsx +0 -76
  116. package/src/components/render-content/render-content.lite.tsx +0 -232
  117. package/src/components/render-inlined-styles.lite.tsx +0 -5
  118. package/src/functions/macro-eval.js +0 -5
  119. package/src/functions/previewing-model-name.js +0 -11
@@ -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.js";
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 state = createMutable({
9
- formState: "unsubmitted",
10
- responseData: null,
11
- formErrorMessage: "",
8
+ const [formState, setFormState] = createSignal("unsubmitted");
9
+ const [responseData, setResponseData] = createSignal(null);
10
+ const [formErrorMessage, setFormErrorMessage] = createSignal("");
12
11
 
13
- get submissionState() {
14
- return isEditing() && props.previewState || state.formState;
15
- },
12
+ function submissionState() {
13
+ return isEditing() && props.previewState || formState();
14
+ }
16
15
 
17
- onSubmit(event) {
18
- const sendWithJs = props.sendWithJs || props.sendSubmissionsTo === "email";
16
+ function onSubmit(event) {
17
+ const sendWithJs = props.sendWithJs || props.sendSubmissionsTo === "email";
19
18
 
20
- if (props.sendSubmissionsTo === "zapier") {
19
+ if (props.sendSubmissionsTo === "zapier") {
20
+ event.preventDefault();
21
+ } else if (sendWithJs) {
22
+ if (!(props.action || props.sendSubmissionsTo === "email")) {
21
23
  event.preventDefault();
22
- } else if (sendWithJs) {
23
- if (!(props.action || props.sendSubmissionsTo === "email")) {
24
- event.preventDefault();
25
- return;
26
- }
24
+ return;
25
+ }
27
26
 
28
- event.preventDefault();
29
- const el = event.currentTarget;
30
- const headers = props.customHeaders || {};
31
- let body;
32
- const formData = new FormData(el); // TODO: maybe support null
33
-
34
- const formPairs = Array.from(event.currentTarget.querySelectorAll("input,select,textarea")).filter(el => !!el.name).map(el => {
35
- let value;
36
- const key = el.name;
37
-
38
- if (el instanceof HTMLInputElement) {
39
- if (el.type === "radio") {
40
- if (el.checked) {
41
- value = el.name;
42
- return {
43
- key,
44
- value
45
- };
46
- }
47
- } else if (el.type === "checkbox") {
48
- value = el.checked;
49
- } else if (el.type === "number" || el.type === "range") {
50
- const num = el.valueAsNumber;
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
- if (!isNaN(num)) {
53
- value = num;
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
- return {
66
- key,
67
- value
68
- };
69
- });
70
- let contentType = props.contentType;
64
+ return {
65
+ key,
66
+ value
67
+ };
68
+ });
69
+ let contentType = props.contentType;
71
70
 
72
- if (props.sendSubmissionsTo === "email") {
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
- if (value instanceof File || Array.isArray(value) && value[0] instanceof File || value instanceof FileList) {
80
- contentType = "multipart/form-data";
81
- }
82
- }); // TODO: send as urlEncoded or multipart by default
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
- if (contentType !== "application/json") {
87
- body = formData;
88
- } else {
89
- // Json
90
- const json = {};
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
- if (contentType && contentType !== "multipart/form-data") {
101
- if (
102
- /* Zapier doesn't allow content-type header to be sent from browsers */
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
- const presubmitEvent = new CustomEvent("presubmit", {
109
- detail: {
110
- body
111
- }
112
- });
113
-
114
- if (formRef) {
115
- formRef.dispatchEvent(presubmitEvent);
113
+ if (formRef) {
114
+ formRef.dispatchEvent(presubmitEvent);
116
115
 
117
- if (presubmitEvent.defaultPrevented) {
118
- return;
119
- }
116
+ if (presubmitEvent.defaultPrevented) {
117
+ return;
120
118
  }
119
+ }
121
120
 
122
- state.formState = "sending";
123
- 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 || "")}`;
124
- fetch(props.sendSubmissionsTo === "email" ? formUrl : props.action,
125
- /* TODO: throw error if no action URL */
126
- {
127
- body,
128
- headers,
129
- method: props.method || "post"
130
- }).then(async res => {
131
- let body;
132
- const contentType = res.headers.get("content-type");
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
- if (!res.ok && props.errorMessagePath) {
141
- /* TODO: allow supplying an error formatter function */
142
- let message = get(body, props.errorMessagePath);
133
+ if (contentType && contentType.indexOf("application/json") !== -1) {
134
+ body = await res.json();
135
+ } else {
136
+ body = await res.text();
137
+ }
143
138
 
144
- if (message) {
145
- if (typeof message !== "string") {
146
- /* TODO: ideally convert json to yaml so it woul dbe like
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
- state.formErrorMessage = message;
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
- state.responseData = body;
156
- state.formState = res.ok ? "success" : "error";
154
+ setResponseData(body);
155
+ setFormState(res.ok ? "success" : "error");
157
156
 
158
- if (res.ok) {
159
- const submitSuccessEvent = new CustomEvent("submit:success", {
160
- detail: {
161
- res,
162
- body
163
- }
164
- });
157
+ if (res.ok) {
158
+ const submitSuccessEvent = new CustomEvent("submit:success", {
159
+ detail: {
160
+ res,
161
+ body
162
+ }
163
+ });
165
164
 
166
- if (formRef) {
167
- formRef.dispatchEvent(submitSuccessEvent);
165
+ if (formRef) {
166
+ formRef.dispatchEvent(submitSuccessEvent);
168
167
 
169
- if (submitSuccessEvent.defaultPrevented) {
170
- return;
171
- }
172
- /* TODO: option to turn this on/off? */
168
+ if (submitSuccessEvent.defaultPrevented) {
169
+ return;
170
+ }
171
+ /* TODO: option to turn this on/off? */
173
172
 
174
173
 
175
- if (props.resetFormOnSubmit !== false) {
176
- formRef.reset();
177
- }
174
+ if (props.resetFormOnSubmit !== false) {
175
+ formRef.reset();
178
176
  }
179
- /* TODO: client side route event first that can be preventDefaulted */
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
- if (!event.defaultPrevented) {
192
- location.href = props.successUrl;
181
+ if (props.successUrl) {
182
+ if (formRef) {
183
+ const event = new CustomEvent("route", {
184
+ detail: {
185
+ url: props.successUrl
193
186
  }
194
- } else {
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
- }, err => {
200
- const submitErrorEvent = new CustomEvent("submit:error", {
201
- detail: {
202
- error: err
203
- }
204
- });
197
+ }
198
+ }, err => {
199
+ const submitErrorEvent = new CustomEvent("submit:error", {
200
+ detail: {
201
+ error: err
202
+ }
203
+ });
205
204
 
206
- if (formRef) {
207
- formRef.dispatchEvent(submitErrorEvent);
205
+ if (formRef) {
206
+ formRef.dispatchEvent(submitErrorEvent);
208
207
 
209
- if (submitErrorEvent.defaultPrevented) {
210
- return;
211
- }
208
+ if (submitErrorEvent.defaultPrevented) {
209
+ return;
212
210
  }
211
+ }
213
212
 
214
- state.responseData = err;
215
- state.formState = "error";
216
- });
217
- }
213
+ setResponseData(err);
214
+ setFormState("error");
215
+ });
218
216
  }
217
+ }
219
218
 
220
- });
221
- const formRef = useRef();
222
- return <form {...props.attributes} validate={props.validate} ref={formRef} action={!props.sendWithJs && props.action} method={props.method} name={props.name} onSubmit={event => state.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={state.submissionState === "error"}>
231
+ <Show when={submissionState() === "error"}>
233
232
  <BuilderBlocks dataPath="errorMessage" blocks={props.errorMessage}></BuilderBlocks>
234
233
  </Show>
235
- <Show when={state.submissionState === "sending"}>
234
+ <Show when={submissionState() === "sending"}>
236
235
  <BuilderBlocks dataPath="sendingMessage" blocks={props.sendingMessage}></BuilderBlocks>
237
236
  </Show>
238
- <Show when={state.submissionState === "error" && state.responseData}>
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(state.responseData, null, 2)}
243
+ {JSON.stringify(responseData(), null, 2)}
245
244
  </pre>
246
245
  </Show>
247
- <Show when={state.submissionState === "success"}>
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: " const DEFAULT_ASPECT_RATIO = 0.7041; options.delete('srcset'); options.delete('noWebp'); function loadImage(url, timeout) { return new Promise((resolve, reject) => { const img = document.createElement('img'); let loaded = false; img.onload = () => { loaded = true; resolve(img); }; img.addEventListener('error', event => { console.warn('Image load failed', event.error); reject(event.error); }); img.src = url; setTimeout(() => { if (!loaded) { reject(new Error('Image load timed out')); } }, timeout); }); } function round(num) { return Math.round(num * 1000) / 1000; } const value = options.get('image'); const aspectRatio = options.get('aspectRatio'); // For SVG images - don't render as webp, keep them as SVG fetch(value) .then(res => res.blob()) .then(blob => { if (blob.type.includes('svg')) { options.set('noWebp', true); } }); if (value && (!aspectRatio || aspectRatio === DEFAULT_ASPECT_RATIO)) { return loadImage(value).then(img => { const possiblyUpdatedAspectRatio = options.get('aspectRatio'); if ( options.get('image') === value && (!possiblyUpdatedAspectRatio || possiblyUpdatedAspectRatio === DEFAULT_ASPECT_RATIO) ) { if (img.width && img.height) { options.set('aspectRatio', round(img.height / img.width)); options.set('height', img.height); options.set('width', img.width); } } }); }"
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
- return <div class={css({
6
- position: "relative"
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} aria-role={props.altText ? "presentation" : undefined} style={{
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={props.srcset} sizes={props.sizes} />
21
- <source srcSet={props.srcset} />
52
+ }} src={props.image} srcset={srcSetToUse()} sizes={props.sizes} />
53
+ <source srcset={srcSetToUse()} />
22
54
  </picture>
23
- <Show when={props.aspectRatio && !(props.fitContent && props.builderBlock?.children?.length)}>
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
- </div>;
82
+ </>;
52
83
  }
53
84
 
54
85
  export default Image;
@@ -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;
@@ -2,6 +2,7 @@ const componentInfo = {
2
2
  name: "Symbol",
3
3
  noWrap: true,
4
4
  static: true,
5
+ builtIn: true,
5
6
  inputs: [
6
7
  {
7
8
  name: "symbol",