@saltcorn/builder 1.6.0-alpha.8 → 1.6.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,7 +75,27 @@ const Card = ({
75
75
  const {
76
76
  selected,
77
77
  connectors: { connect, drag },
78
- } = useNode((node) => ({ selected: node.events.selected }));
78
+ mobileWidth,
79
+ tabletWidth,
80
+ mobileHeight,
81
+ tabletHeight,
82
+ } = useNode((node) => ({
83
+ selected: node.events.selected,
84
+ _style: node.data.props.style,
85
+ mobileWidth: node.data.props.mobileWidth,
86
+ tabletWidth: node.data.props.tabletWidth,
87
+ mobileHeight: node.data.props.mobileHeight,
88
+ tabletHeight: node.data.props.tabletHeight,
89
+ }));
90
+ const { previewDevice } = useContext(previewCtx);
91
+ const deviceSizeOverrides = {};
92
+ if (previewDevice === "mobile") {
93
+ if (mobileWidth) deviceSizeOverrides.width = mobileWidth;
94
+ if (mobileHeight) deviceSizeOverrides.height = mobileHeight;
95
+ } else if (previewDevice === "tablet") {
96
+ if (tabletWidth) deviceSizeOverrides.width = tabletWidth;
97
+ if (tabletHeight) deviceSizeOverrides.height = tabletHeight;
98
+ }
79
99
 
80
100
  return (
81
101
  <div
@@ -105,6 +125,7 @@ const Card = ({
105
125
  }deg, ${gradStartColor}, ${gradEndColor})`,
106
126
  }
107
127
  : {}),
128
+ ...deviceSizeOverrides,
108
129
  }}
109
130
  ref={(dom) => connect(drag(dom))}
110
131
  >
@@ -486,6 +507,10 @@ const fields = [
486
507
  { label: "Title Right", name: "titleRight", type: "Nodes", nodeID: "titleRight" },
487
508
  { label: "Footer", name: "footer", type: "Nodes", nodeID: "cardfooter" },
488
509
  { name: "style", default: {} },
510
+ { name: "mobileWidth" },
511
+ { name: "tabletWidth" },
512
+ { name: "mobileHeight" },
513
+ { name: "tabletHeight" },
489
514
  { label: "Class", name: "class", type: "String", canBeFormula: true },
490
515
  { name: "hAlign" },
491
516
  { name: "bgType" },
@@ -4,14 +4,14 @@
4
4
  * @subcategory components / elements
5
5
  */
6
6
 
7
- import React, { Fragment } from "react";
7
+ import React, { Fragment, useContext } from "react";
8
8
  import { Column } from "./Column";
9
9
  import useTranslation from "../../hooks/useTranslation";
10
+ import PreviewCtx from "../preview_context";
10
11
 
11
12
  import { Element, useNode } from "@craftjs/core";
12
13
  import {
13
14
  Accordion,
14
- ConfigField,
15
15
  SettingsRow,
16
16
  reactifyStyles,
17
17
  SettingsSectionHeaderRow,
@@ -19,6 +19,8 @@ import {
19
19
  parseStyles,
20
20
  } from "./utils";
21
21
  import { BoxModelEditor } from "./BoxModelEditor";
22
+ import { ArrayManager } from "./ArrayManager";
23
+ import { getAlignClass } from "../../utils/responsive_utils";
22
24
  import {
23
25
  AlignTop,
24
26
  AlignMiddle,
@@ -66,6 +68,35 @@ const resetWidths = (ncols) => ntimes(ncols - 1, () => Math.floor(12 / ncols));
66
68
  const getWidth = (widths, colix) =>
67
69
  colix < widths.length ? widths[colix] : 12 - sum(widths);
68
70
 
71
+ /**
72
+ * Bootstrap breakpoint minimum widths (px)
73
+ */
74
+ const BREAKPOINT_MIN_WIDTH = {
75
+ "": 0,
76
+ sm: 576,
77
+ md: 768,
78
+ lg: 992,
79
+ xl: 1200,
80
+ };
81
+
82
+ const PREVIEW_DEVICE_WIDTH = {
83
+ desktop: Infinity,
84
+ tablet: 768,
85
+ mobile: 576,
86
+ };
87
+
88
+ const getColClass = (width, breakpoint, previewDevice) => {
89
+ if (!previewDevice || previewDevice === "desktop") {
90
+ const bp = breakpoint || "sm";
91
+ return bp ? `col-${bp}-${width}` : `col-${width}`;
92
+ }
93
+ const deviceWidth = PREVIEW_DEVICE_WIDTH[previewDevice] || Infinity;
94
+ const bpMin = BREAKPOINT_MIN_WIDTH[breakpoint || "sm"] || 0;
95
+ if (deviceWidth < bpMin) return "col-12";
96
+ const bp = breakpoint || "sm";
97
+ return bp ? `col-${bp}-${width}` : `col-${width}`;
98
+ };
99
+
69
100
  export /**
70
101
  * @param {object} opts
71
102
  * @param {number[]} opts.widths
@@ -84,28 +115,55 @@ const Columns = ({
84
115
  gx,
85
116
  gy,
86
117
  aligns,
118
+ mobileAligns,
119
+ tabletAligns,
87
120
  vAligns,
88
121
  colClasses,
89
122
  colStyles,
90
123
  customClass,
124
+ breakpoints,
91
125
  }) => {
92
126
  const {
93
127
  selected,
94
128
  connectors: { connect, drag },
95
- } = useNode((node) => ({ selected: node.events.selected }));
129
+ mobileWidth,
130
+ tabletWidth,
131
+ mobileHeight,
132
+ tabletHeight,
133
+ } = useNode((node) => ({
134
+ selected: node.events.selected,
135
+ _style: node.data.props.style,
136
+ mobileWidth: node.data.props.mobileWidth,
137
+ tabletWidth: node.data.props.tabletWidth,
138
+ mobileHeight: node.data.props.mobileHeight,
139
+ tabletHeight: node.data.props.tabletHeight,
140
+ }));
141
+ const { previewDevice } = useContext(PreviewCtx);
142
+ const canvasStyle = { ...reactifyStyles(style || {}) };
143
+ if (previewDevice === "mobile") {
144
+ if (mobileWidth) canvasStyle.width = mobileWidth;
145
+ if (mobileHeight) canvasStyle.height = mobileHeight;
146
+ } else if (previewDevice === "tablet") {
147
+ if (tabletWidth) canvasStyle.width = tabletWidth;
148
+ if (tabletHeight) canvasStyle.height = tabletHeight;
149
+ }
96
150
  return (
97
151
  <div
98
152
  className={`row builder-columns ${customClass || ""} ${selected ? "selected-node" : ""} ${
99
153
  typeof gx !== "undefined" && gx !== null ? `gx-${gx}` : ""
100
154
  } ${typeof gy !== "undefined" && gy !== null ? `gy-${gy}` : ""}`}
101
155
  ref={(dom) => connect(drag(dom))}
102
- style={reactifyStyles(style || {})}
156
+ style={canvasStyle}
103
157
  >
104
158
  {ntimes(ncols, (ix) => (
105
159
  <div
106
160
  key={ix}
107
- className={`split-col col-sm-${getWidth(widths, ix)} text-${
108
- aligns?.[ix]
161
+ className={`split-col ${getColClass(
162
+ getWidth(widths, ix),
163
+ breakpoints?.[ix],
164
+ previewDevice
165
+ )} ${
166
+ getAlignClass(aligns, mobileAligns, tabletAligns, ix, previewDevice)
109
167
  } align-items-${vAligns?.[ix]} ${colClasses?.[ix] || ""}`}
110
168
  style={parseStyles(colStyles?.[ix] || "")}
111
169
  >
@@ -126,16 +184,23 @@ export /**
126
184
  */
127
185
  const ColumnsSettings = () => {
128
186
  const { t } = useTranslation();
187
+ const { previewDevice } = useContext(PreviewCtx);
129
188
  const node = useNode((node) => ({
130
189
  widths: node.data.props.widths,
131
190
  ncols: node.data.props.ncols,
132
191
  breakpoints: node.data.props.breakpoints,
133
192
  style: node.data.props.style,
193
+ mobileWidth: node.data.props.mobileWidth,
194
+ tabletWidth: node.data.props.tabletWidth,
195
+ mobileHeight: node.data.props.mobileHeight,
196
+ tabletHeight: node.data.props.tabletHeight,
134
197
  setting_col_n: node.data.props.setting_col_n,
135
198
  gx: node.data.props.gx,
136
199
  gy: node.data.props.gy,
137
200
  vAligns: node.data.props.vAligns,
138
201
  aligns: node.data.props.aligns,
202
+ mobileAligns: node.data.props.mobileAligns,
203
+ tabletAligns: node.data.props.tabletAligns,
139
204
  colClasses: node.data.props.colClasses,
140
205
  colStyles: node.data.props.colStyles,
141
206
  customClass: node.data.props.customClass,
@@ -150,118 +215,99 @@ const ColumnsSettings = () => {
150
215
  setting_col_n,
151
216
  vAligns,
152
217
  aligns,
218
+ mobileAligns,
219
+ tabletAligns,
153
220
  colClasses,
154
221
  colStyles,
155
222
  customClass,
156
223
  currentSettingsTab,
157
224
  } = node;
225
+
226
+ const activeAlignProp =
227
+ previewDevice === "mobile" ? "mobileAligns" :
228
+ previewDevice === "tablet" ? "tabletAligns" : "aligns";
229
+ const activeAligns =
230
+ previewDevice === "mobile" ? mobileAligns :
231
+ previewDevice === "tablet" ? tabletAligns : aligns;
232
+
158
233
  const colSetsNode = {
159
- vAlign: vAligns?.[setting_col_n - 1],
160
- hAlign: aligns?.[setting_col_n - 1],
161
- colClass: colClasses?.[setting_col_n - 1] || "",
162
- colStyle: colStyles?.[setting_col_n - 1] || "",
234
+ vAlign: vAligns?.[setting_col_n],
235
+ hAlign: activeAligns?.[setting_col_n],
236
+ colClass: colClasses?.[setting_col_n] || "",
237
+ colStyle: colStyles?.[setting_col_n] || "",
163
238
  };
164
239
  return (
165
240
  <Accordion
166
241
  value={currentSettingsTab}
167
242
  onChange={(ix) => setProp((prop) => (prop.currentSettingsTab = ix))}
168
243
  >
169
- <table accordiontitle={t("Column properties")}>
170
- <tbody>
171
- <tr>
172
- <td colSpan="3">
173
- <label>{t("Number of columns")}</label>
174
- </td>
175
- <td>
176
- <input
177
- type="number"
178
- value={ncols}
179
- className="form-control"
180
- step="1"
181
- min="1"
182
- max="6"
183
- onChange={(e) => {
184
- if (!e.target) return;
185
- const value = e.target.value;
186
- setProp((prop) => {
187
- prop.ncols = value;
188
- prop.widths = resetWidths(value);
189
- });
190
- }}
191
- />
192
- </td>
193
- </tr>
194
- <tr>
195
- <th colSpan="4">{t("Widths & Breakpoint")}</th>
196
- </tr>
197
- {ntimes(ncols, (ix) => (
198
- <Fragment key={ix}>
199
- <tr>
200
- <th colSpan="4">Column {ix + 1}</th>
201
- </tr>
202
- <tr>
203
- <td>
204
- <label>{t("Width")}</label>
205
- </td>
206
- <td align="right">
207
- {ix < ncols - 1 ? (
208
- <input
209
- type="number"
210
- value={widths[ix]}
211
- className="form-control"
212
- step="1"
213
- min="1"
214
- max={12 - (sum(widths) - widths[ix]) - 1}
215
- onChange={(e) => {
216
- if (!e.target) return;
217
- const value = e.target.value;
218
- setProp((prop) => (prop.widths[ix] = +value));
219
- }}
220
- />
221
- ) : (
222
- `${12 - sum(widths)}`
223
- )}
224
- </td>
225
- <td>/12</td>
226
- <td>
227
- <select
228
- className="form-control form-select"
229
- value={breakpoints[ix]}
244
+ <div accordiontitle={t("Column properties")}>
245
+ <ArrayManager
246
+ node={node}
247
+ setProp={setProp}
248
+ countProp={"ncols"}
249
+ currentProp={"setting_col_n"}
250
+ managedArrays={["widths", "breakpoints", "aligns", "mobileAligns", "tabletAligns", "vAligns", "colClasses", "colStyles"]}
251
+ manageContents={true}
252
+ contentsKey={"besides"}
253
+ initialAddProps={{
254
+ breakpoints: "sm",
255
+ }}
256
+ onLayoutChange={(layout, action) => {
257
+ if (action === "add" || action === "delete") {
258
+ const n = layout.besides.length;
259
+ layout.widths = ntimes(n, () => Math.floor(12 / n));
260
+ }
261
+ }}
262
+ />
263
+ <table className="w-100 mt-2">
264
+ <tbody>
265
+ <tr>
266
+ <th colSpan="4">{t("Width & Breakpoint")}</th>
267
+ </tr>
268
+ <tr>
269
+ <td>
270
+ <label>{t("Width")}</label>
271
+ </td>
272
+ <td colSpan="3" align="right">
273
+ {setting_col_n < ncols - 1 ? (
274
+ <input
275
+ type="number"
276
+ value={widths[setting_col_n]}
277
+ className="form-control"
278
+ step="1"
279
+ min="1"
280
+ max={12 - (sum(widths) - widths[setting_col_n]) - 1}
230
281
  onChange={(e) => {
231
282
  if (!e.target) return;
232
283
  const value = e.target.value;
233
- setProp((prop) => (prop.breakpoints[ix] = value));
284
+ setProp((prop) => (prop.widths[setting_col_n] = +value));
234
285
  }}
235
- >
236
- <option disabled>{t("Breakpoint")}</option>
237
- <option value="">{t("none")}</option>
238
- {buildBootstrapOptions(["sm", "md", "lg"])}
239
- </select>
240
- </td>
241
- </tr>
242
- </Fragment>
243
- ))}
244
- </tbody>
245
- </table>
246
- <div accordiontitle={t("Column settings")}>
247
- {t("Settings for column #")}
248
- <ConfigField
249
- field={{
250
- name: "setting_col_n",
251
- label: t("Column number"),
252
- type: "btn_select",
253
- options: ntimes(ncols, (i) => ({
254
- value: i + 1,
255
- title: `${i + 1}`,
256
- label: `${i + 1}`,
257
- })),
258
- }}
259
- node={node}
260
- setProp={setProp}
261
- props={node}
262
- ></ConfigField>
263
- <table className="w-100">
264
- <tbody>
286
+ />
287
+ ) : (
288
+ `${12 - sum(widths)}`
289
+ )}
290
+ </td>
291
+ </tr>
292
+ <tr>
293
+ <td>
294
+ <label>{t("Breakpoint")}</label>
295
+ </td>
296
+ <td colSpan="3">
297
+ <select
298
+ className="form-control form-select"
299
+ value={breakpoints[setting_col_n]}
300
+ onChange={(e) => {
301
+ if (!e.target) return;
302
+ const value = e.target.value;
303
+ setProp((prop) => (prop.breakpoints[setting_col_n] = value));
304
+ }}
305
+ >
306
+ <option value="">{t("none")}</option>
307
+ {buildBootstrapOptions(["sm", "md", "lg"])}
308
+ </select>
309
+ </td>
310
+ </tr>
265
311
  <SettingsSectionHeaderRow title={t("Align")} />
266
312
  <SettingsRow
267
313
  field={{
@@ -279,14 +325,16 @@ const ColumnsSettings = () => {
279
325
  onChange={(k, v) =>
280
326
  setProp((prop) => {
281
327
  if (!prop.vAligns) prop.vAligns = [];
282
- prop.vAligns[setting_col_n - 1] = v;
328
+ prop.vAligns[setting_col_n] = v;
283
329
  })
284
330
  }
285
331
  />
286
332
  <SettingsRow
287
333
  field={{
288
334
  name: "hAlign",
289
- label: t("Horizontal"),
335
+ label: previewDevice !== "desktop"
336
+ ? `${t("Horizontal")} (${previewDevice})`
337
+ : t("Horizontal"),
290
338
  type: "btn_select",
291
339
  options: [
292
340
  { value: "start", title: t("Left"), label: <AlignStart /> },
@@ -298,8 +346,8 @@ const ColumnsSettings = () => {
298
346
  setProp={setProp}
299
347
  onChange={(k, v) =>
300
348
  setProp((prop) => {
301
- if (!prop.aligns) prop.aligns = [];
302
- prop.aligns[setting_col_n - 1] = v;
349
+ if (!prop[activeAlignProp]) prop[activeAlignProp] = [];
350
+ prop[activeAlignProp][setting_col_n] = v;
303
351
  })
304
352
  }
305
353
  />
@@ -314,7 +362,7 @@ const ColumnsSettings = () => {
314
362
  onChange={(k, v) =>
315
363
  setProp((prop) => {
316
364
  if (!prop.colClasses) prop.colClasses = [];
317
- prop.colClasses[setting_col_n - 1] = v;
365
+ prop.colClasses[setting_col_n] = v;
318
366
  })
319
367
  }
320
368
  />
@@ -322,14 +370,14 @@ const ColumnsSettings = () => {
322
370
  field={{
323
371
  name: "colStyle",
324
372
  label: t("CSS"),
325
- type: "textarea",
373
+ type: "String",
326
374
  }}
327
375
  node={colSetsNode}
328
376
  setProp={setProp}
329
377
  onChange={(k, v) =>
330
378
  setProp((prop) => {
331
379
  if (!prop.colStyles) prop.colStyles = [];
332
- prop.colStyles[setting_col_n - 1] = v;
380
+ prop.colStyles[setting_col_n] = v;
333
381
  })
334
382
  }
335
383
  />
@@ -384,7 +432,7 @@ Columns.craft = {
384
432
  ncols: 2,
385
433
  style: {},
386
434
  breakpoints: ["sm", "sm"],
387
- setting_col_n: 1,
435
+ setting_col_n: 0,
388
436
  customClass: "",
389
437
  },
390
438
  related: {
@@ -135,7 +135,30 @@ const Container = ({
135
135
  const {
136
136
  selected,
137
137
  connectors: { connect, drag },
138
- } = useNode((node) => ({ selected: node.events.selected }));
138
+ mobileWidth,
139
+ tabletWidth,
140
+ mobileHeight,
141
+ tabletHeight,
142
+ } = useNode((node) => ({
143
+ selected: node.events.selected,
144
+ mobileWidth: node.data.props.mobileWidth,
145
+ tabletWidth: node.data.props.tabletWidth,
146
+ mobileHeight: node.data.props.mobileHeight,
147
+ tabletHeight: node.data.props.tabletHeight,
148
+ }));
149
+ const { previewDevice } = useContext(previewCtx);
150
+
151
+ const BP_MIN = { "": 0, sm: 576, md: 768, lg: 992, xl: 1200 };
152
+ const DEVICE_W = { desktop: Infinity, tablet: 768, mobile: 576 };
153
+ {
154
+ const dw = DEVICE_W[previewDevice] || Infinity;
155
+ if (minScreenWidth && dw < (BP_MIN[minScreenWidth] || 0)) {
156
+ return <div ref={(dom) => connect(drag(dom))} style={{ display: "none" }} />;
157
+ }
158
+ if (maxScreenWidth && dw >= (BP_MIN[maxScreenWidth] || Infinity)) {
159
+ return <div ref={(dom) => connect(drag(dom))} style={{ display: "none" }} />;
160
+ }
161
+ }
139
162
 
140
163
  const actualChildren = contents || children;
141
164
 
@@ -162,9 +185,7 @@ const Container = ({
162
185
  style: {
163
186
  ...parseStyles(customCSS || ""),
164
187
  ...reactifyStyles(style, transform, rotate),
165
- display,
166
- //padding: padding.map((p) => p + "px").join(" "),
167
- //margin: margin.map((p) => p + "px").join(" "),
188
+ ...(display && display !== "block" ? { display } : {}),
168
189
  minHeight: minHeight ? `${minHeight}${minHeightUnit || "px"}` : null,
169
190
  ...(bgType === "Image" && bgFileId
170
191
  ? {
@@ -196,19 +217,25 @@ const Container = ({
196
217
  color: textColor,
197
218
  }
198
219
  : {}),
199
- ...(typeof height !== "undefined"
220
+ ...(height
200
221
  ? {
201
222
  height: `${height}${heightUnit || "px"}`,
202
223
  }
203
224
  : {}),
204
- ...(typeof width !== "undefined"
225
+ ...(width
205
226
  ? {
206
227
  width: `${width}${widthUnit || "px"}`,
207
228
  }
208
229
  : {}),
230
+ ...(previewDevice === "mobile" && mobileWidth ? { width: mobileWidth } : {}),
231
+ ...(previewDevice === "mobile" && mobileHeight ? { height: mobileHeight } : {}),
232
+ ...(previewDevice === "tablet" && tabletWidth ? { width: tabletWidth } : {}),
233
+ ...(previewDevice === "tablet" && tabletHeight ? { height: tabletHeight } : {}),
209
234
  },
210
235
  },
211
- React.createElement(Element, { canvas: true, id: "container-canvas", is: Column }, renderChildren())
236
+ <Element canvas id="container-canvas" is={Column}>
237
+ {renderChildren()}
238
+ </Element>
212
239
  );
213
240
  };
214
241
 
@@ -225,6 +252,10 @@ const ContainerSettings = () => {
225
252
  minHeight: node.data.props.minHeight,
226
253
  height: node.data.props.height,
227
254
  width: node.data.props.width,
255
+ mobileWidth: node.data.props.mobileWidth,
256
+ tabletWidth: node.data.props.tabletWidth,
257
+ mobileHeight: node.data.props.mobileHeight,
258
+ tabletHeight: node.data.props.tabletHeight,
228
259
  minHeightUnit: node.data.props.minHeightUnit,
229
260
  heightUnit: node.data.props.heightUnit,
230
261
  widthUnit: node.data.props.widthUnit,
@@ -1213,7 +1244,7 @@ Container.craft = {
1213
1244
  showIfFormula: "",
1214
1245
  showForRole: [],
1215
1246
  margin: [0, 0, 0, 0],
1216
- padding: [16, 16, 16, 16],
1247
+ padding: [0, 0, 0, 0],
1217
1248
  minScreenWidth: "",
1218
1249
  display: "block",
1219
1250
  show_for_owner: false,
@@ -1246,6 +1277,10 @@ Container.craft = {
1246
1277
  { name: "minHeight", default: 20 },
1247
1278
  { name: "height" },
1248
1279
  { name: "width" },
1280
+ { name: "mobileWidth" },
1281
+ { name: "tabletWidth" },
1282
+ { name: "mobileHeight" },
1283
+ { name: "tabletHeight" },
1249
1284
  { name: "click_action" },
1250
1285
  { name: "url", canBeFormula: true },
1251
1286
  { name: "hoverColor" },