@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.
- package/dist/builder_bundle.js +7 -7
- package/dist/builder_bundle.js.LICENSE.txt +2 -0
- package/package.json +3 -2
- package/src/components/Builder.js +367 -186
- package/src/components/RenderNode.js +21 -3
- package/src/components/Toolbox.js +100 -22
- package/src/components/elements/Action.js +11 -121
- package/src/components/elements/ArrayManager.js +10 -5
- package/src/components/elements/BoxModelEditor.js +24 -23
- package/src/components/elements/Card.js +26 -1
- package/src/components/elements/Columns.js +158 -110
- package/src/components/elements/Container.js +43 -8
- package/src/components/elements/CustomLayer.js +285 -0
- package/src/components/elements/DropDownFilter.js +8 -1
- package/src/components/elements/DropMenu.js +10 -4
- package/src/components/elements/HTMLCode.js +3 -1
- package/src/components/elements/MonacoEditor.js +120 -15
- package/src/components/elements/Prompt.js +285 -0
- package/src/components/elements/SearchBar.js +28 -5
- package/src/components/elements/Table.js +10 -12
- package/src/components/elements/Text.js +59 -15
- package/src/components/elements/View.js +19 -7
- package/src/components/elements/ViewLink.js +1 -0
- package/src/components/elements/utils.js +133 -30
- package/src/components/storage.js +33 -7
- package/src/index.js +10 -0
- package/src/utils/responsive_utils.js +139 -0
|
@@ -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
|
|
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
|
-
{" "}
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
+
};
|