@saltcorn/builder 1.6.0-alpha.10 → 1.6.0-alpha.12
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 +86 -4126
- package/package.json +2 -2
- package/src/components/Builder.js +126 -48
- package/src/components/RenderNode.js +8 -1
- package/src/components/elements/ArrayManager.js +10 -5
- package/src/components/elements/Columns.js +112 -105
- package/src/components/elements/Container.js +16 -3
- package/src/components/storage.js +6 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/builder",
|
|
3
|
-
"version": "1.6.0-alpha.
|
|
3
|
+
"version": "1.6.0-alpha.12",
|
|
4
4
|
"description": "Drag and drop view builder for Saltcorn, open-source no-code platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"homepage": "https://saltcorn.com",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@fortawesome/free-solid-svg-icons": "5.15.2",
|
|
31
31
|
"@fortawesome/react-fontawesome": "0.1.14",
|
|
32
32
|
"@monaco-editor/react": "4.7.0",
|
|
33
|
-
"@saltcorn/common-code": "1.6.0-alpha.
|
|
33
|
+
"@saltcorn/common-code": "1.6.0-alpha.12",
|
|
34
34
|
"@tippyjs/react": "4.2.6",
|
|
35
35
|
"babel-jest": "^29.7.0",
|
|
36
36
|
"babel-loader": "9.2.1",
|
|
@@ -12,6 +12,7 @@ import React, {
|
|
|
12
12
|
useRef,
|
|
13
13
|
memo,
|
|
14
14
|
} from "react";
|
|
15
|
+
import { createPortal } from "react-dom";
|
|
15
16
|
import useTranslation from "../hooks/useTranslation";
|
|
16
17
|
import { Editor, Frame, Element, Selector, useEditor, DefaultEventHandlers } from "@craftjs/core";
|
|
17
18
|
import { Layers, useLayer } from "@craftjs/layers"
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
faCaretSquareRight,
|
|
68
69
|
} from "@fortawesome/free-regular-svg-icons";
|
|
69
70
|
import { Accordion, ErrorBoundary } from "./elements/utils";
|
|
71
|
+
import { Display, Tablet, Phone } from "react-bootstrap-icons";
|
|
70
72
|
import { InitNewElement, Library, LibraryElem } from "./Library";
|
|
71
73
|
import { RenderNode } from "./RenderNode";
|
|
72
74
|
import { ListColumn } from "./elements/ListColumn";
|
|
@@ -104,7 +106,7 @@ const getFirstSelected = (selected) => {
|
|
|
104
106
|
* @subcategory components
|
|
105
107
|
* @namespace
|
|
106
108
|
*/
|
|
107
|
-
const SettingsPanel = () => {
|
|
109
|
+
const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
108
110
|
const { t } = useTranslation();
|
|
109
111
|
const options = useContext(optionsCtx);
|
|
110
112
|
|
|
@@ -160,6 +162,15 @@ const SettingsPanel = () => {
|
|
|
160
162
|
const tagName = target.tagName.toLowerCase();
|
|
161
163
|
const hasSelection = selectedCount > 0;
|
|
162
164
|
if ((tagName === "body" || tagName === "button") && hasSelection) {
|
|
165
|
+
if (!selected && selectedCount > 1 && (keyCode === 8 || keyCode === 46)) {
|
|
166
|
+
const currentSelected = query.getEvent("selected");
|
|
167
|
+
const nodeIds = getSelectedNodes(currentSelected)
|
|
168
|
+
.map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
|
|
169
|
+
.filter((nodeId) => nodeId && nodeId !== "ROOT");
|
|
170
|
+
nodeIds.forEach((nodeId) => {
|
|
171
|
+
try { actions.delete(nodeId); } catch (e) { /* node may already be deleted */ }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
163
174
|
if (selected) {
|
|
164
175
|
if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
|
|
165
176
|
deleteChildren();
|
|
@@ -298,6 +309,14 @@ const SettingsPanel = () => {
|
|
|
298
309
|
actions.history.redo();
|
|
299
310
|
}
|
|
300
311
|
}
|
|
312
|
+
if ((tagName === "body" || tagName === "button") &&
|
|
313
|
+
(event.ctrlKey || event.metaKey) && event.keyCode == 65) {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
const rootChildren = query.node("ROOT").childNodes();
|
|
316
|
+
if (rootChildren.length > 0) {
|
|
317
|
+
actions.selectNode(rootChildren);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
301
320
|
};
|
|
302
321
|
useEffect(() => {
|
|
303
322
|
window.addEventListener("keydown", handleUserKeyPress);
|
|
@@ -336,13 +355,23 @@ const SettingsPanel = () => {
|
|
|
336
355
|
|
|
337
356
|
return (
|
|
338
357
|
<div className="settings-panel card mt-1">
|
|
339
|
-
<div className="card-header px-2 py-1">
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
<div className="card-header px-2 py-1 d-flex justify-content-between align-items-center">
|
|
359
|
+
<div>
|
|
360
|
+
{selected && selected.displayName ? (
|
|
361
|
+
<Fragment>
|
|
362
|
+
<b>{selected.displayName}</b> settings
|
|
363
|
+
</Fragment>
|
|
364
|
+
) : (
|
|
365
|
+
t("Settings")
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
{setIsEnlarged && (
|
|
369
|
+
<FontAwesomeIcon
|
|
370
|
+
icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
|
|
371
|
+
className="fa-lg builder-expand-toggle-right"
|
|
372
|
+
onClick={() => setIsEnlarged(!isEnlarged)}
|
|
373
|
+
title={isEnlarged ? t("Shrink") : t("Enlarge")}
|
|
374
|
+
/>
|
|
346
375
|
)}
|
|
347
376
|
</div>
|
|
348
377
|
<div className="card-body p-2">
|
|
@@ -426,7 +455,7 @@ function useWindowDimensions() {
|
|
|
426
455
|
* @namespace
|
|
427
456
|
*/
|
|
428
457
|
|
|
429
|
-
const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu"]);
|
|
458
|
+
const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu", "ListColumn"]);
|
|
430
459
|
|
|
431
460
|
const CustomLayerComponent = memo(({ children }) => {
|
|
432
461
|
const {
|
|
@@ -443,7 +472,7 @@ const CustomLayerComponent = memo(({ children }) => {
|
|
|
443
472
|
};
|
|
444
473
|
});
|
|
445
474
|
|
|
446
|
-
const { displayName, hasNodes, isHiddenColumn, connectors: editorConnectors } = useEditor((state) => {
|
|
475
|
+
const { displayName, hasNodes, isHiddenColumn, selected, connectors: editorConnectors } = useEditor((state) => {
|
|
447
476
|
const node = state.nodes[id];
|
|
448
477
|
const data = node?.data;
|
|
449
478
|
|
|
@@ -469,28 +498,33 @@ const CustomLayerComponent = memo(({ children }) => {
|
|
|
469
498
|
}
|
|
470
499
|
}
|
|
471
500
|
|
|
501
|
+
const isSelected = state.events?.selected?.has?.(id) || (state.events?.selected === id);
|
|
502
|
+
|
|
472
503
|
return {
|
|
473
504
|
displayName: name,
|
|
474
505
|
hasNodes: hasChildren,
|
|
475
|
-
isHiddenColumn: shouldHide
|
|
506
|
+
isHiddenColumn: shouldHide,
|
|
507
|
+
selected: isSelected
|
|
476
508
|
};
|
|
477
509
|
});
|
|
478
510
|
|
|
511
|
+
const isRoot = id === "ROOT";
|
|
512
|
+
|
|
479
513
|
// Auto-expand hidden linked-node Columns so their children are always
|
|
480
514
|
// visible through the transparent wrapper. Uses setExpandedState(true)
|
|
481
515
|
// instead of toggleLayer() — it's idempotent (no-op when already true),
|
|
482
516
|
// so it won't conflict with craft.js internals or cause toggle loops.
|
|
483
517
|
useEffect(() => {
|
|
484
|
-
if (isHiddenColumn && !expanded) {
|
|
518
|
+
if ((isHiddenColumn || isRoot) && !expanded) {
|
|
485
519
|
setExpandedState(true);
|
|
486
520
|
}
|
|
487
|
-
}, [isHiddenColumn, expanded, setExpandedState]);
|
|
521
|
+
}, [isHiddenColumn, isRoot, expanded, setExpandedState]);
|
|
488
522
|
|
|
489
|
-
if (isHiddenColumn) {
|
|
523
|
+
if (isHiddenColumn || isRoot) {
|
|
490
524
|
return (
|
|
491
525
|
<div
|
|
492
526
|
ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}
|
|
493
|
-
style={{ marginLeft: "-
|
|
527
|
+
style={{ marginLeft: "-14px" }}
|
|
494
528
|
>
|
|
495
529
|
{children}
|
|
496
530
|
</div>
|
|
@@ -501,9 +535,9 @@ const CustomLayerComponent = memo(({ children }) => {
|
|
|
501
535
|
<div ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}>
|
|
502
536
|
<div
|
|
503
537
|
ref={(dom) => { drag(dom); layerHeader(dom); }}
|
|
504
|
-
className={`builder-layer-node ${hovered ? "hovered" : ""}`}
|
|
538
|
+
className={`builder-layer-node ${hovered ? "hovered" : ""} ${selected ? "selected" : ""}`}
|
|
505
539
|
style={{
|
|
506
|
-
paddingLeft: `${depth *
|
|
540
|
+
paddingLeft: `${depth * 14 + 10}px`,
|
|
507
541
|
}}
|
|
508
542
|
>
|
|
509
543
|
<span className="layer-name" style={{ flexGrow: 1 }}>{displayName}</span>
|
|
@@ -545,6 +579,38 @@ const AddColumnButton = () => {
|
|
|
545
579
|
);
|
|
546
580
|
};
|
|
547
581
|
|
|
582
|
+
const DEVICE_WIDTHS = {
|
|
583
|
+
desktop: null,
|
|
584
|
+
tablet: 768,
|
|
585
|
+
mobile: 375,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const DevicePreviewToolbar = ({ previewDevice, setPreviewDevice }) => {
|
|
589
|
+
const { t } = useTranslation();
|
|
590
|
+
const devices = [
|
|
591
|
+
{ key: "desktop", icon: Display, label: t("Desktop") },
|
|
592
|
+
{ key: "tablet", icon: Tablet, label: t("Tablet") },
|
|
593
|
+
{ key: "mobile", icon: Phone, label: t("Mobile") },
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<div className="device-preview-toolbar">
|
|
598
|
+
{devices.map(({ key, icon: Icon, label }) => (
|
|
599
|
+
<button
|
|
600
|
+
key={key}
|
|
601
|
+
className={`btn btn-sm ${
|
|
602
|
+
previewDevice === key ? "btn-primary" : "btn-outline-secondary"
|
|
603
|
+
} device-preview-btn`}
|
|
604
|
+
onClick={() => setPreviewDevice(key)}
|
|
605
|
+
title={label}
|
|
606
|
+
>
|
|
607
|
+
<Icon size={16} />
|
|
608
|
+
</button>
|
|
609
|
+
))}
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
|
|
548
614
|
/**
|
|
549
615
|
* @returns {Fragment}
|
|
550
616
|
* @category saltcorn-builder
|
|
@@ -644,6 +710,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
644
710
|
const [isEnlarged, setIsEnlarged] = useState(false);
|
|
645
711
|
const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
|
|
646
712
|
const [relationsCache, setRelationsCache] = useState({});
|
|
713
|
+
const [previewDevice, setPreviewDevice] = useState("desktop");
|
|
647
714
|
const { windowWidth, windowHeight } = useWindowDimensions();
|
|
648
715
|
|
|
649
716
|
const [builderHeight, setBuilderHeight] = useState(0);
|
|
@@ -703,7 +770,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
703
770
|
>
|
|
704
771
|
<Provider value={options}>
|
|
705
772
|
<PreviewCtx.Provider
|
|
706
|
-
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
|
|
773
|
+
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles, previewDevice }}
|
|
707
774
|
>
|
|
708
775
|
<RelationsCtx.Provider
|
|
709
776
|
value={{
|
|
@@ -782,40 +849,51 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
782
849
|
options.mode !== "list" ? "emptymsg" : ""
|
|
783
850
|
}`}
|
|
784
851
|
>
|
|
785
|
-
<div>
|
|
786
|
-
<
|
|
787
|
-
{
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
852
|
+
<div className="device-preview-scroll-area">
|
|
853
|
+
<div
|
|
854
|
+
className={`device-preview-canvas-wrapper ${
|
|
855
|
+
previewDevice !== "desktop" ? "device-preview-constrained" : ""
|
|
856
|
+
}`}
|
|
857
|
+
style={{
|
|
858
|
+
maxWidth: DEVICE_WIDTHS[previewDevice]
|
|
859
|
+
? `${DEVICE_WIDTHS[previewDevice]}px`
|
|
860
|
+
: "none",
|
|
861
|
+
}}
|
|
862
|
+
>
|
|
863
|
+
<Frame>
|
|
864
|
+
{options.mode === "list" ? (
|
|
865
|
+
<Element canvas is={ListColumns}></Element>
|
|
866
|
+
) : (
|
|
867
|
+
<Element canvas is={Column}></Element>
|
|
868
|
+
)}
|
|
869
|
+
</Frame>
|
|
870
|
+
{options.mode === "list" ? <AddColumnButton /> : null}
|
|
871
|
+
</div>
|
|
794
872
|
</div>
|
|
795
873
|
</div>
|
|
796
874
|
<div className="col-sm-auto builder-sidebar">
|
|
797
875
|
<div style={{ width: isEnlarged ? "28rem" : "16rem" }}>
|
|
798
|
-
<
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
icon={faSave}
|
|
802
|
-
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
803
|
-
/>
|
|
804
|
-
<FontAwesomeIcon
|
|
805
|
-
icon={faExclamationTriangle}
|
|
806
|
-
color="#ff0033"
|
|
807
|
-
className={savingState.error ? "d-inline" : "d-none"}
|
|
808
|
-
/>
|
|
809
|
-
<FontAwesomeIcon
|
|
810
|
-
icon={
|
|
811
|
-
isEnlarged ? faCaretSquareRight : faCaretSquareLeft
|
|
812
|
-
}
|
|
813
|
-
className={
|
|
814
|
-
"float-end me-2 mt-1 fa-lg builder-expand-toggle-right"
|
|
815
|
-
}
|
|
816
|
-
onClick={() => setIsEnlarged(!isEnlarged)}
|
|
817
|
-
title={isEnlarged ? t("Shrink") : t("Enlarge")}
|
|
876
|
+
<DevicePreviewToolbar
|
|
877
|
+
previewDevice={previewDevice}
|
|
878
|
+
setPreviewDevice={setPreviewDevice}
|
|
818
879
|
/>
|
|
880
|
+
{document.getElementById("builder-header-actions") &&
|
|
881
|
+
createPortal(
|
|
882
|
+
<Fragment>
|
|
883
|
+
<FontAwesomeIcon
|
|
884
|
+
icon={faSave}
|
|
885
|
+
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
886
|
+
/>
|
|
887
|
+
<FontAwesomeIcon
|
|
888
|
+
icon={faExclamationTriangle}
|
|
889
|
+
color="#ff0033"
|
|
890
|
+
className={savingState.error ? "d-inline" : "d-none"}
|
|
891
|
+
/>
|
|
892
|
+
<HistoryPanel />
|
|
893
|
+
<NextButton layout={layout} />
|
|
894
|
+
</Fragment>,
|
|
895
|
+
document.getElementById("builder-header-actions")
|
|
896
|
+
)}
|
|
819
897
|
<div
|
|
820
898
|
className={` ${
|
|
821
899
|
savingState.error ? "d-block" : "d-none"
|
|
@@ -823,7 +901,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
823
901
|
>
|
|
824
902
|
{t("your work is not being saved")}
|
|
825
903
|
</div>
|
|
826
|
-
<SettingsPanel />
|
|
904
|
+
<SettingsPanel isEnlarged={isEnlarged} setIsEnlarged={setIsEnlarged} />
|
|
827
905
|
</div>
|
|
828
906
|
</div>
|
|
829
907
|
</div>
|
|
@@ -82,6 +82,7 @@ const RenderNode = ({ render }) => {
|
|
|
82
82
|
|
|
83
83
|
const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu"]);
|
|
84
84
|
useEffect(() => {
|
|
85
|
+
if (!isActive) return;
|
|
85
86
|
if (name === "Column" && parent && parent !== "ROOT") {
|
|
86
87
|
const parentNode = query.node(parent).get();
|
|
87
88
|
const parentName = parentNode?.data?.displayName;
|
|
@@ -91,7 +92,13 @@ const RenderNode = ({ render }) => {
|
|
|
91
92
|
parentLinked &&
|
|
92
93
|
Object.values(parentLinked).includes(id)
|
|
93
94
|
) {
|
|
94
|
-
|
|
95
|
+
const currentlySelected = query.getEvent("selected").all();
|
|
96
|
+
const otherSelected = currentlySelected.filter((nid) => nid !== id);
|
|
97
|
+
if (otherSelected.length > 0) {
|
|
98
|
+
actions.selectNode([...otherSelected, parent]);
|
|
99
|
+
} else {
|
|
100
|
+
actions.selectNode(parent);
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
}, [isActive]);
|
|
@@ -56,6 +56,8 @@ export const ArrayManager = ({
|
|
|
56
56
|
managedArrays,
|
|
57
57
|
manageContents,
|
|
58
58
|
initialAddProps,
|
|
59
|
+
contentsKey = "contents",
|
|
60
|
+
onLayoutChange,
|
|
59
61
|
}) => {
|
|
60
62
|
const { t } = useTranslation();
|
|
61
63
|
const { actions, query, connectors } = useEditor((state, query) => {
|
|
@@ -77,13 +79,14 @@ export const ArrayManager = ({
|
|
|
77
79
|
node.id,
|
|
78
80
|
options
|
|
79
81
|
);
|
|
80
|
-
layout.
|
|
82
|
+
layout[contentsKey].splice(rmIx, 1);
|
|
81
83
|
|
|
82
84
|
managedArrays.forEach((arrNm) => {
|
|
83
|
-
|
|
85
|
+
if (layout[arrNm]) layout[arrNm].splice(rmIx, 1);
|
|
84
86
|
});
|
|
85
87
|
layout[countProp] = node[countProp] - 1;
|
|
86
|
-
layout[currentProp] = node[currentProp] - 1;
|
|
88
|
+
layout[currentProp] = Math.max(0, node[currentProp] - 1);
|
|
89
|
+
if (onLayoutChange) onLayoutChange(layout, "delete");
|
|
87
90
|
actions.delete(node.id);
|
|
88
91
|
layoutToNodes(layout, query, actions, parentId, options, sibIx);
|
|
89
92
|
} else {
|
|
@@ -119,7 +122,7 @@ export const ArrayManager = ({
|
|
|
119
122
|
options
|
|
120
123
|
);
|
|
121
124
|
|
|
122
|
-
swapElements(layout
|
|
125
|
+
swapElements(layout[contentsKey], curIx, curIx + delta);
|
|
123
126
|
|
|
124
127
|
managedArrays.forEach((arrNm) => {
|
|
125
128
|
if (arrNm.includes(".")) {
|
|
@@ -130,6 +133,7 @@ export const ArrayManager = ({
|
|
|
130
133
|
swapElements(layout[arrNm], curIx, curIx + delta);
|
|
131
134
|
});
|
|
132
135
|
layout[currentProp] = node[currentProp] + delta;
|
|
136
|
+
if (onLayoutChange) onLayoutChange(layout, "move");
|
|
133
137
|
actions.delete(node.id);
|
|
134
138
|
layoutToNodes(layout, query, actions, parentId, options, sibIx);
|
|
135
139
|
} else
|
|
@@ -156,7 +160,7 @@ export const ArrayManager = ({
|
|
|
156
160
|
options
|
|
157
161
|
);
|
|
158
162
|
|
|
159
|
-
layout.
|
|
163
|
+
layout[contentsKey].push(null);
|
|
160
164
|
managedArrays.forEach((arrNm) => {
|
|
161
165
|
if (initialAddProps?.[arrNm])
|
|
162
166
|
layout[arrNm][node[countProp]] = initialAddProps?.[arrNm];
|
|
@@ -164,6 +168,7 @@ export const ArrayManager = ({
|
|
|
164
168
|
layout[currentProp] = +node[countProp];
|
|
165
169
|
layout[countProp] = +node[countProp] + 1;
|
|
166
170
|
|
|
171
|
+
if (onLayoutChange) onLayoutChange(layout, "add");
|
|
167
172
|
actions.delete(node.id);
|
|
168
173
|
layoutToNodes(layout, query, actions, parentId, options, sibIx);
|
|
169
174
|
} else
|