@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.
@@ -748,12 +748,16 @@ export const fetchFieldPreview =
748
748
  ...(args.configuration || {}),
749
749
  ...(changes.configuration || {}),
750
750
  };
751
+ const body = { configuration };
752
+ if (options.mode === "show" && options.current_filter_state?.id) {
753
+ body.row_id = options.current_filter_state.id;
754
+ }
751
755
  fetchPreview({
752
756
  options,
753
757
  node_id,
754
758
  setPreviews,
755
759
  url: `/field/preview/${options.tableName}/${name}/${fieldview}`,
756
- body: { configuration },
760
+ body,
757
761
  });
758
762
  };
759
763
 
@@ -932,6 +936,95 @@ const ColorInput = ({ value, onChange }) =>
932
936
  </button>
933
937
  );
934
938
 
939
+ const CodeFieldWithModal = ({ value, onChange, setProp, mode, label, hideLabel }) => {
940
+ const [modalOpen, setModalOpen] = useState(false);
941
+ const { t } = useTranslation();
942
+ return (
943
+ <Fragment>
944
+ {!hideLabel && (
945
+ <label>
946
+ {t(label)}{" "}
947
+ <i
948
+ className="fas fa-external-link-alt ms-1"
949
+ style={{ cursor: "pointer" }}
950
+ onClick={() => setModalOpen(true)}
951
+ title={t("Open code popup")}
952
+ ></i>
953
+ </label>
954
+ )}
955
+ {hideLabel && (
956
+ <i
957
+ className="fas fa-external-link-alt ms-1"
958
+ style={{ cursor: "pointer" }}
959
+ onClick={() => setModalOpen(true)}
960
+ title={t("Open code popup")}
961
+ ></i>
962
+ )}
963
+ <MultiLineCodeEditor
964
+ setProp={setProp}
965
+ value={value}
966
+ onChange={onChange}
967
+ mode={mode}
968
+ />
969
+ {modalOpen ? (
970
+ <div
971
+ className={`modal fade show`}
972
+ style={{ display: "block", zIndex: 1055 }}
973
+ tabIndex={-1}
974
+ role="dialog"
975
+ aria-labelledby="codeModalLabel"
976
+ aria-hidden={false}
977
+ >
978
+ <div
979
+ className="modal-backdrop fade show"
980
+ style={{ zIndex: 1050 }}
981
+ onClick={() => setModalOpen(false)}
982
+ aria-hidden="true"
983
+ />
984
+ <div
985
+ className="modal-dialog modal-dialog-centered modal-lg"
986
+ role="document"
987
+ style={{ zIndex: 1060 }}
988
+ onClick={(e) => e.stopPropagation()}
989
+ >
990
+ <div className="modal-content code-modal">
991
+ <div className="modal-header">
992
+ <h5 className="modal-title" id="codeModalLabel">
993
+ {t(label)}
994
+ </h5>
995
+ <button
996
+ type="button"
997
+ className="btn-close"
998
+ aria-label="Close"
999
+ onClick={() => setModalOpen(false)}
1000
+ />
1001
+ </div>
1002
+ <div className="modal-body">
1003
+ <MultiLineCodeEditor
1004
+ setProp={setProp}
1005
+ value={value}
1006
+ onChange={onChange}
1007
+ isModalEditor
1008
+ mode={mode}
1009
+ />
1010
+ </div>
1011
+ <div className="modal-footer">
1012
+ <button
1013
+ type="button"
1014
+ className="btn btn-secondary"
1015
+ onClick={() => setModalOpen(false)}
1016
+ >
1017
+ {t("Close")}
1018
+ </button>
1019
+ </div>
1020
+ </div>
1021
+ </div>
1022
+ </div>
1023
+ ) : null}
1024
+ </Fragment>
1025
+ );
1026
+ };
1027
+
935
1028
  export /**
936
1029
  * @param {object} props
937
1030
  * @param {object[]} props.fields
@@ -953,7 +1046,6 @@ const ConfigForm = ({
953
1046
  onChange,
954
1047
  tableName,
955
1048
  fieldName,
956
- openPopup
957
1049
  }) => (
958
1050
  <div className="form-namespace">
959
1051
  {fields.map((f, ix) => {
@@ -968,7 +1060,7 @@ const ConfigForm = ({
968
1060
  }
969
1061
  return (
970
1062
  <div key={ix} className="builder-config-field" data-field-name={f.name}>
971
- {!isCheckbox(f) ? (
1063
+ {!isCheckbox(f) && f.input_type !== "code" ? (
972
1064
  <label>
973
1065
  {f.label || f.name}
974
1066
  {f.help ? (
@@ -978,7 +1070,7 @@ const ConfigForm = ({
978
1070
  table_name={tableName}
979
1071
  />
980
1072
  ) : null}
981
- {" "}{openPopup && <i class="fas fa-external-link-alt " onClick={openPopup}></i>}
1073
+ {" "}
982
1074
  </label>
983
1075
  ) : null}
984
1076
  <ConfigField
@@ -1045,7 +1137,7 @@ const ConfigField = ({
1045
1137
  */
1046
1138
  const options = React.useContext(optionsCtx);
1047
1139
 
1048
- const myOnChange = (v0) => {
1140
+ const myOnChange = (v0, throttleRate) => {
1049
1141
  const v = valuePostfix && (v0 || v0 === 0) ? v0 + valuePostfix : v0;
1050
1142
  setProp((prop) => {
1051
1143
  if (setter) setter(prop, field.name, v);
@@ -1059,7 +1151,7 @@ const ConfigField = ({
1059
1151
  if (!prop[subProp]) prop[subProp] = {};
1060
1152
  prop[subProp][field.name] = v;
1061
1153
  } else prop[field.name] = v;
1062
- });
1154
+ }, throttleRate);
1063
1155
  onChange && onChange(field.name, v, setProp);
1064
1156
  apply_showif();
1065
1157
  };
@@ -1100,7 +1192,7 @@ const ConfigField = ({
1100
1192
  typeof stored_value === "undefined"
1101
1193
  ) {
1102
1194
  useEffect(() => {
1103
- myOnChange(field.default);
1195
+ myOnChange(field.default, 500);
1104
1196
  }, []);
1105
1197
  } else if (hasSelect && typeof value === "undefined") {
1106
1198
  //pick first value to mimic html form behaviour
@@ -1108,7 +1200,7 @@ const ConfigField = ({
1108
1200
  let o;
1109
1201
  if (options && (o = options[0]))
1110
1202
  useEffect(() => {
1111
- myOnChange(typeof o === "string" ? o : o.value || o.name || o);
1203
+ myOnChange(typeof o === "string" ? o : o.value || o.name || o, 500);
1112
1204
  }, []);
1113
1205
  }
1114
1206
 
@@ -1233,25 +1325,33 @@ const ConfigField = ({
1233
1325
  onChange={(e) => e.target && myOnChange(e.target.value)}
1234
1326
  />
1235
1327
  ),
1236
- code: () =>
1237
- field?.attributes?.expression_type === "row" ||
1238
- field?.attributes?.expression_type === "query" ? (
1239
- <textarea
1240
- rows="6"
1241
- type="text"
1242
- className={`field-${field?.name} form-control`}
1243
- value={value}
1244
- name={field?.name}
1245
- onChange={(e) => e.target && myOnChange(e.target.value)}
1246
- spellCheck={false}
1247
- />
1248
- ) : (
1249
- <MultiLineCodeEditor
1250
- setProp={setProp}
1328
+ code: () => {
1329
+ if (
1330
+ field?.attributes?.expression_type === "row" ||
1331
+ field?.attributes?.expression_type === "query"
1332
+ ) {
1333
+ return (
1334
+ <textarea
1335
+ rows="6"
1336
+ type="text"
1337
+ className={`field-${field?.name} form-control`}
1338
+ value={value}
1339
+ name={field?.name}
1340
+ onChange={(e) => e.target && myOnChange(e.target.value)}
1341
+ spellCheck={false}
1342
+ />
1343
+ );
1344
+ }
1345
+ return (
1346
+ <CodeFieldWithModal
1251
1347
  value={value}
1252
1348
  onChange={myOnChange}
1349
+ setProp={setProp}
1350
+ mode={field?.attributes?.mode}
1351
+ label={field?.label || field?.name || "Code"}
1253
1352
  />
1254
- ),
1353
+ );
1354
+ },
1255
1355
  select: () => {
1256
1356
  if (field.class?.includes?.("selectizable")) {
1257
1357
  const seloptions = field.options.map((o, ix) =>
@@ -1350,7 +1450,9 @@ const ConfigField = ({
1350
1450
  e?.target &&
1351
1451
  myOnChange(
1352
1452
  isStyle || subProp
1353
- ? `${e.target.value}${styleDim || "px"}`
1453
+ ? e.target.value === ""
1454
+ ? ""
1455
+ : `${e.target.value}${styleDim || "px"}`
1354
1456
  : e.target.value
1355
1457
  )
1356
1458
  }
@@ -1498,7 +1600,7 @@ const SettingsRow = ({
1498
1600
  valuePostfix,
1499
1601
  }) => {
1500
1602
  const { t } = useTranslation();
1501
- const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
1603
+ const fullWidth = ["String", "Bool", "textarea"].includes(field.type) || field.input_type === "code";
1502
1604
  const needLabel = field.type !== "Bool";
1503
1605
  const inner = field.canBeFormula ? (
1504
1606
  <OrFormula
@@ -1528,7 +1630,7 @@ const SettingsRow = ({
1528
1630
  <tr>
1529
1631
  {fullWidth ? (
1530
1632
  <td colSpan="2">
1531
- {needLabel && <label>{field.label}</label>}
1633
+ {needLabel && field.input_type !== "code" && <label>{field.label}</label>}
1532
1634
  {inner}
1533
1635
  {field.sublabel ? (
1534
1636
  <i
@@ -1722,9 +1824,10 @@ const ButtonOrLinkSettingsRows = ({
1722
1824
  <td>
1723
1825
  <FontIconPicker
1724
1826
  value={values[keyPrefix + "icon"]}
1725
- onChange={(value) =>
1726
- setProp((prop) => (prop[keyPrefix + "icon"] = value))
1727
- }
1827
+ onChange={(value) => {
1828
+ if ((value || "") !== (values[keyPrefix + "icon"] || ""))
1829
+ setProp((prop) => (prop[keyPrefix + "icon"] = value), 500);
1830
+ }}
1728
1831
  isMulti={false}
1729
1832
  icons={faIcons || []}
1730
1833
  />
@@ -30,6 +30,7 @@ import { DropDownFilter } from "./elements/DropDownFilter";
30
30
  import { ToggleFilter } from "./elements/ToggleFilter";
31
31
  import { DropMenu } from "./elements/DropMenu";
32
32
  import { Container } from "./elements/Container";
33
+ import { Prompt } from "./elements/Prompt";
33
34
  import { rand_ident } from "./elements/utils";
34
35
 
35
36
  /**
@@ -80,6 +81,7 @@ const allElements = [
80
81
  Table,
81
82
  ListColumn,
82
83
  ListColumns,
84
+ Prompt,
83
85
  ];
84
86
 
85
87
  export /**
@@ -106,7 +108,7 @@ const layoutToNodes = (
106
108
  * @returns {Element|Text|View|Action|Tabs|Columns}
107
109
  */
108
110
  function toTag(segment, ix) {
109
- if (!segment) return <Empty key={ix} />;
111
+ if (!segment) return null;
110
112
 
111
113
  if (
112
114
  (segment.type === "card" || segment.type === "container") &&
@@ -183,6 +185,8 @@ const layoutToNodes = (
183
185
  style={segment.style || {}}
184
186
  icon={segment.icon}
185
187
  font={segment.font || ""}
188
+ mobileFontSize={segment.mobileFontSize}
189
+ tabletFontSize={segment.tabletFontSize}
186
190
  />
187
191
  );
188
192
  } else if (segment.type === "view") {
@@ -318,7 +322,13 @@ const layoutToNodes = (
318
322
  colClasses={segment.colClasses}
319
323
  colStyles={segment.colStyles}
320
324
  aligns={segment.aligns}
321
- setting_col_n={1}
325
+ mobileAligns={segment.mobileAligns}
326
+ tabletAligns={segment.tabletAligns}
327
+ mobileWidth={segment.mobileWidth}
328
+ tabletWidth={segment.tabletWidth}
329
+ mobileHeight={segment.mobileHeight}
330
+ tabletHeight={segment.tabletHeight}
331
+ setting_col_n={segment.setting_col_n !== undefined ? segment.setting_col_n : 0}
322
332
  contents={segment.besides.map(toTag)}
323
333
  />
324
334
  );
@@ -354,7 +364,13 @@ const layoutToNodes = (
354
364
  colClasses={segment.colClasses}
355
365
  colStyles={segment.colStyles}
356
366
  aligns={segment.aligns}
357
- setting_col_n={1}
367
+ mobileAligns={segment.mobileAligns}
368
+ tabletAligns={segment.tabletAligns}
369
+ mobileWidth={segment.mobileWidth}
370
+ tabletWidth={segment.tabletWidth}
371
+ mobileHeight={segment.mobileHeight}
372
+ tabletHeight={segment.tabletHeight}
373
+ setting_col_n={segment.setting_col_n !== undefined ? segment.setting_col_n : 0}
358
374
  contents={segment.besides.map(toTag)}
359
375
  />
360
376
  )
@@ -414,8 +430,9 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
414
430
  const go = (node) => {
415
431
  if (!node) return;
416
432
  let customProps = {};
417
- if (Object.keys(node?.custom || {}).length)
418
- customProps = { _custom: { ...node?.custom } };
433
+ const mergedCustom = { ...(node?.props?.custom || {}), ...(node?.custom || {}) };
434
+ if (Object.keys(mergedCustom).length)
435
+ customProps = { _custom: { ...mergedCustom } };
419
436
  const matchElement = allElements.find(
420
437
  (e) =>
421
438
  e.craft.related &&
@@ -489,6 +506,8 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
489
506
  style: node.props.style,
490
507
  icon: node.props.icon,
491
508
  font: node.props.font,
509
+ mobileFontSize: node.props.mobileFontSize,
510
+ tabletFontSize: node.props.tabletFontSize,
492
511
  ...customProps,
493
512
  };
494
513
  }
@@ -522,14 +541,21 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
522
541
  besides: widths.map((w, ix) => go(nodes[node.linkedNodes["Col" + ix]])),
523
542
  breakpoints: node.props.breakpoints,
524
543
  customClass: node.props.customClass,
525
- gx: +node.props.gx,
526
- gy: +node.props.gy,
544
+ gx: node.props.gx != null ? +node.props.gx : undefined,
545
+ gy: node.props.gy != null ? +node.props.gy : undefined,
527
546
  aligns: node.props.aligns,
547
+ mobileAligns: node.props.mobileAligns,
548
+ tabletAligns: node.props.tabletAligns,
528
549
  vAligns: node.props.vAligns,
529
550
  colClasses: node.props.colClasses,
530
551
  colStyles: node.props.colStyles,
531
552
  style: node.props.style,
553
+ mobileWidth: node.props.mobileWidth,
554
+ tabletWidth: node.props.tabletWidth,
555
+ mobileHeight: node.props.mobileHeight,
556
+ tabletHeight: node.props.tabletHeight,
532
557
  widths,
558
+ setting_col_n: node.props.setting_col_n,
533
559
  ...customProps,
534
560
  };
535
561
  }
package/src/index.js CHANGED
@@ -50,8 +50,18 @@
50
50
 
51
51
  import React from "react";
52
52
  import { createRoot } from "react-dom/client";
53
+ import { polyfill } from "mobile-drag-drop";
54
+ import { scrollBehaviourDragImageTranslateOverride } from "mobile-drag-drop/scroll-behaviour";
53
55
  import Builder from "./components/Builder";
54
56
 
57
+
58
+ polyfill({
59
+ forceApply: true,
60
+ dragImageTranslateOverride: scrollBehaviourDragImageTranslateOverride,
61
+ });
62
+
63
+ window.addEventListener("touchmove", function () {}, { passive: false });
64
+
55
65
  /**
56
66
  *
57
67
  * @param {object} id
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Get the active alignment value for the current preview device.
3
+ * Used in the builder to show the correct alignment per device.
4
+ *
5
+ * @param {string[]} aligns - Desktop alignment array (per column)
6
+ * @param {string[]} mobileAligns - Mobile alignment array (per column)
7
+ * @param {string[]} tabletAligns - Tablet alignment array (per column)
8
+ * @param {number} ix - Column index
9
+ * @param {string} previewDevice - Current preview device ("desktop"|"tablet"|"mobile")
10
+ * @returns {string} CSS class like "text-start" or ""
11
+ */
12
+ export const getAlignClass = (aligns, mobileAligns, tabletAligns, ix, previewDevice) => {
13
+ const desktop = aligns?.[ix];
14
+ const tablet = tabletAligns?.[ix];
15
+ const mobile = mobileAligns?.[ix];
16
+
17
+ if (previewDevice === "mobile" && mobile) return `text-${mobile}`;
18
+ if (previewDevice === "tablet" && tablet) return `text-${tablet}`;
19
+ if (desktop) return `text-${desktop}`;
20
+ return "";
21
+ };
22
+
23
+ /**
24
+ * Generate Bootstrap responsive text alignment classes for runtime rendering.
25
+ * Uses mobile-first approach: base class for smallest, then breakpoint overrides.
26
+ *
27
+ * @param {string[]} aligns - Desktop alignment array (per column)
28
+ * @param {string[]} mobileAligns - Mobile alignment array (per column)
29
+ * @param {string[]} tabletAligns - Tablet alignment array (per column)
30
+ * @param {number} ix - Column index
31
+ * @returns {string} Space-separated CSS classes like "text-start text-md-center text-lg-end"
32
+ */
33
+ export const getAlignClassRuntime = (aligns, mobileAligns, tabletAligns, ix) => {
34
+ const desktop = aligns?.[ix];
35
+ const tablet = tabletAligns?.[ix];
36
+ const mobile = mobileAligns?.[ix];
37
+
38
+ if (!mobile && !tablet) {
39
+ return desktop ? `text-${desktop}` : "";
40
+ }
41
+
42
+ const classes = [];
43
+
44
+ const base = mobile || desktop;
45
+ if (base) classes.push(`text-${base}`);
46
+ if (tablet && tablet !== base) classes.push(`text-md-${tablet}`);
47
+ if (desktop && desktop !== (tablet || base))
48
+ classes.push(`text-lg-${desktop}`);
49
+ return classes.join(" ");
50
+ };
51
+
52
+ /**
53
+ * Get the active value for a per-device setting based on the preview device.
54
+ * Generic helper for any setting that supports per-device overrides.
55
+ *
56
+ * @param {*} desktopValue - Desktop value
57
+ * @param {*} tabletValue - Tablet override (optional)
58
+ * @param {*} mobileValue - Mobile override (optional)
59
+ * @param {string} previewDevice - Current preview device
60
+ * @returns {*} The active value for the current device
61
+ */
62
+ export const getDeviceValue = (desktopValue, tabletValue, mobileValue, previewDevice) => {
63
+ if (previewDevice === "mobile" && mobileValue != null) return mobileValue;
64
+ if (previewDevice === "tablet" && tabletValue != null) return tabletValue;
65
+ return desktopValue;
66
+ };
67
+
68
+ /**
69
+ * Get the device-specific property name for a given base property.
70
+ * E.g. getDevicePropName("width", "mobile") => "mobileWidth"
71
+ *
72
+ * @param {string} propName - Base property name (e.g. "width", "height")
73
+ * @param {string} previewDevice - Current preview device ("mobile"|"tablet")
74
+ * @returns {string} Device-specific property name
75
+ */
76
+ export const getDevicePropName = (propName, previewDevice) => {
77
+ const cap = propName.charAt(0).toUpperCase() + propName.slice(1);
78
+ return previewDevice === "mobile" ? `mobile${cap}` : `tablet${cap}`;
79
+ };
80
+
81
+ /**
82
+ * Create a proxied node object that reads width/height from device-specific props
83
+ * as if they were in the style object. Used by BoxModelEditor for per-device size editing.
84
+ *
85
+ * @param {object} node - The Craft.js node props
86
+ * @param {string} propName - "width" or "height"
87
+ * @param {string} previewDevice - Current preview device
88
+ * @returns {object} Proxied node with device value in style[propName]
89
+ */
90
+ export const getDeviceSizeNode = (node, propName, previewDevice) => {
91
+ const deviceProp = getDevicePropName(propName, previewDevice);
92
+ return {
93
+ ...node,
94
+ style: { ...(node.style || {}), [propName]: node[deviceProp] || "" },
95
+ };
96
+ };
97
+
98
+ /**
99
+ * Create a proxied setProp function that writes to device-specific props
100
+ * instead of the style object. Used by BoxModelEditor for per-device size editing.
101
+ *
102
+ * @param {function} setProp - The Craft.js setProp function
103
+ * @param {string} propName - "width" or "height"
104
+ * @param {string} previewDevice - Current preview device
105
+ * @returns {function} Proxied setProp that intercepts style writes
106
+ */
107
+ export const getDeviceSizeSetProp = (setProp, propName, previewDevice) => {
108
+ const deviceProp = getDevicePropName(propName, previewDevice);
109
+ return (fn) => {
110
+ const proxy = { style: {} };
111
+ fn(proxy);
112
+ const val = proxy.style[propName];
113
+ if (val !== undefined) {
114
+ setProp((prop) => { prop[deviceProp] = val; });
115
+ }
116
+ };
117
+ };
118
+
119
+ /**
120
+ * Get the display value for width/height in the box model visual,
121
+ * taking into account the current preview device and sizeWithStyle mode.
122
+ *
123
+ * @param {object} node - The Craft.js node props
124
+ * @param {string} propName - "width" or "height"
125
+ * @param {string} previewDevice - Current preview device
126
+ * @param {boolean} sizeWithStyle - Whether size is stored in style object
127
+ * @returns {string} Display value like "200px" or ""
128
+ */
129
+ export const getDisplaySize = (node, propName, previewDevice, sizeWithStyle) => {
130
+ const isDesktop = !previewDevice || previewDevice === "desktop";
131
+ if (!isDesktop) {
132
+ const deviceProp = getDevicePropName(propName, previewDevice);
133
+ if (node[deviceProp]) return node[deviceProp];
134
+ }
135
+ if (sizeWithStyle) return (node.style || {})[propName];
136
+ const val = node[propName];
137
+ const unit = node[propName + "Unit"];
138
+ return val ? `${val}${unit || "px"}` : "";
139
+ };