@saltcorn/builder 1.6.0-alpha.1 → 1.6.0-alpha.11
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 +85 -1
- package/dist/builder_bundle.js.LICENSE.txt +18 -51
- package/package.json +31 -27
- package/src/components/Builder.js +445 -155
- package/src/components/Library.js +25 -13
- package/src/components/RenderNode.js +26 -8
- package/src/components/Toolbox.js +333 -269
- package/src/components/elements/Action.js +144 -29
- package/src/components/elements/Aggregation.js +20 -23
- package/src/components/elements/ArrayManager.js +17 -10
- package/src/components/elements/BoxModelEditor.js +19 -17
- package/src/components/elements/Card.js +47 -34
- package/src/components/elements/Clone.js +74 -2
- package/src/components/elements/Column.js +1 -1
- package/src/components/elements/Columns.js +130 -121
- package/src/components/elements/Container.js +185 -92
- package/src/components/elements/DropDownFilter.js +10 -8
- package/src/components/elements/DropMenu.js +18 -9
- package/src/components/elements/Field.js +9 -7
- package/src/components/elements/HTMLCode.js +3 -1
- package/src/components/elements/Image.js +20 -15
- package/src/components/elements/JoinField.js +15 -11
- package/src/components/elements/Link.js +18 -16
- package/src/components/elements/ListColumn.js +7 -3
- package/src/components/elements/ListColumns.js +4 -1
- package/src/components/elements/MonacoEditor.js +4 -2
- package/src/components/elements/Page.js +7 -4
- package/src/components/elements/RelationBadges.js +16 -11
- package/src/components/elements/RelationOnDemandPicker.js +18 -12
- package/src/components/elements/SearchBar.js +37 -10
- package/src/components/elements/Table.js +72 -65
- package/src/components/elements/Tabs.js +18 -15
- package/src/components/elements/Text.js +19 -14
- package/src/components/elements/ToggleFilter.js +28 -25
- package/src/components/elements/View.js +36 -18
- package/src/components/elements/ViewLink.js +15 -11
- package/src/components/elements/utils.js +224 -55
- package/src/components/storage.js +33 -134
- package/src/hooks/useTranslation.js +11 -0
- package/src/index.js +6 -3
|
@@ -10,8 +10,12 @@ import React, {
|
|
|
10
10
|
useState,
|
|
11
11
|
Fragment,
|
|
12
12
|
useRef,
|
|
13
|
+
memo,
|
|
13
14
|
} from "react";
|
|
14
|
-
import {
|
|
15
|
+
import { createPortal } from "react-dom";
|
|
16
|
+
import useTranslation from "../hooks/useTranslation";
|
|
17
|
+
import { Editor, Frame, Element, Selector, useEditor, DefaultEventHandlers } from "@craftjs/core";
|
|
18
|
+
import { Layers, useLayer } from "@craftjs/layers"
|
|
15
19
|
import { Text } from "./elements/Text";
|
|
16
20
|
import { Field } from "./elements/Field";
|
|
17
21
|
import { JoinField } from "./elements/JoinField";
|
|
@@ -46,7 +50,7 @@ import { Link } from "./elements/Link";
|
|
|
46
50
|
import { View } from "./elements/View";
|
|
47
51
|
import { Container } from "./elements/Container";
|
|
48
52
|
import { Column } from "./elements/Column";
|
|
49
|
-
import { Layers } from "saltcorn-craft-layers-noeye";
|
|
53
|
+
// import { Layers } from "saltcorn-craft-layers-noeye";
|
|
50
54
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
51
55
|
import {
|
|
52
56
|
faCopy,
|
|
@@ -56,13 +60,16 @@ import {
|
|
|
56
60
|
faSave,
|
|
57
61
|
faExclamationTriangle,
|
|
58
62
|
faPlus,
|
|
63
|
+
faChevronDown,
|
|
64
|
+
faChevronUp
|
|
59
65
|
} from "@fortawesome/free-solid-svg-icons";
|
|
60
66
|
import {
|
|
61
67
|
faCaretSquareLeft,
|
|
62
68
|
faCaretSquareRight,
|
|
63
69
|
} from "@fortawesome/free-regular-svg-icons";
|
|
64
70
|
import { Accordion, ErrorBoundary } from "./elements/utils";
|
|
65
|
-
import {
|
|
71
|
+
import { Display, Tablet, Phone } from "react-bootstrap-icons";
|
|
72
|
+
import { InitNewElement, Library, LibraryElem } from "./Library";
|
|
66
73
|
import { RenderNode } from "./RenderNode";
|
|
67
74
|
import { ListColumn } from "./elements/ListColumn";
|
|
68
75
|
import { ListColumns } from "./elements/ListColumns";
|
|
@@ -70,6 +77,28 @@ import { recursivelyCloneToElems } from "./elements/Clone";
|
|
|
70
77
|
|
|
71
78
|
const { Provider } = optionsCtx;
|
|
72
79
|
|
|
80
|
+
const getSelectedNodes = (selected) => {
|
|
81
|
+
if (!selected) return [];
|
|
82
|
+
if (typeof selected.all === "function") {
|
|
83
|
+
return selected.all();
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(selected.all)) {
|
|
86
|
+
return selected.all;
|
|
87
|
+
}
|
|
88
|
+
if (typeof selected.values === "function") {
|
|
89
|
+
return Array.from(selected.values());
|
|
90
|
+
}
|
|
91
|
+
if (typeof selected.has === "function") {
|
|
92
|
+
return [...selected];
|
|
93
|
+
}
|
|
94
|
+
return [selected];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getFirstSelected = (selected) => {
|
|
98
|
+
const nodes = getSelectedNodes(selected);
|
|
99
|
+
return nodes.length > 0 ? nodes[0] : null;
|
|
100
|
+
};
|
|
101
|
+
|
|
73
102
|
/**
|
|
74
103
|
*
|
|
75
104
|
* @returns {div}
|
|
@@ -77,11 +106,13 @@ const { Provider } = optionsCtx;
|
|
|
77
106
|
* @subcategory components
|
|
78
107
|
* @namespace
|
|
79
108
|
*/
|
|
80
|
-
const SettingsPanel = () => {
|
|
109
|
+
const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
110
|
+
const { t } = useTranslation();
|
|
81
111
|
const options = useContext(optionsCtx);
|
|
82
112
|
|
|
83
|
-
const { actions, selected, query } = useEditor((state, query) => {
|
|
84
|
-
const
|
|
113
|
+
const { actions, selected, selectedCount, query } = useEditor((state, query) => {
|
|
114
|
+
const selectedNodes = getSelectedNodes(state.events.selected);
|
|
115
|
+
const currentNodeId = selectedNodes.length === 1 ? selectedNodes[0] : null;
|
|
85
116
|
let selected;
|
|
86
117
|
|
|
87
118
|
if (currentNodeId) {
|
|
@@ -104,6 +135,7 @@ const SettingsPanel = () => {
|
|
|
104
135
|
|
|
105
136
|
return {
|
|
106
137
|
selected,
|
|
138
|
+
selectedCount: selectedNodes.length,
|
|
107
139
|
};
|
|
108
140
|
});
|
|
109
141
|
|
|
@@ -128,74 +160,135 @@ const SettingsPanel = () => {
|
|
|
128
160
|
const handleUserKeyPress = (event) => {
|
|
129
161
|
const { keyCode, target } = event;
|
|
130
162
|
const tagName = target.tagName.toLowerCase();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if ((keyCode === 8 || keyCode === 46)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (prevSib) actions.selectNode(prevSib);
|
|
142
|
-
else actions.selectNode(parent);
|
|
143
|
-
}
|
|
144
|
-
if (keyCode === 46) {
|
|
145
|
-
//del
|
|
146
|
-
const nextSib = otherSibling(1);
|
|
147
|
-
deleteThis();
|
|
148
|
-
if (nextSib) actions.selectNode(nextSib);
|
|
163
|
+
const hasSelection = selectedCount > 0;
|
|
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
|
+
});
|
|
149
173
|
}
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
if (selected) {
|
|
175
|
+
if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
|
|
176
|
+
deleteChildren();
|
|
177
|
+
}
|
|
178
|
+
if (keyCode === 8) {
|
|
179
|
+
//backspace
|
|
180
|
+
const prevSib = otherSibling(-1);
|
|
181
|
+
const parent = selected.parent;
|
|
182
|
+
deleteThis();
|
|
183
|
+
if (prevSib) actions.selectNode(prevSib);
|
|
184
|
+
else actions.selectNode(parent);
|
|
185
|
+
}
|
|
186
|
+
if (keyCode === 46) {
|
|
187
|
+
//del
|
|
188
|
+
const nextSib = otherSibling(1);
|
|
189
|
+
deleteThis();
|
|
190
|
+
if (nextSib) actions.selectNode(nextSib);
|
|
191
|
+
}
|
|
192
|
+
if (keyCode === 37 && selected.parent)
|
|
193
|
+
//left
|
|
194
|
+
actions.selectNode(selected.parent);
|
|
195
|
+
|
|
196
|
+
if (keyCode === 39) {
|
|
197
|
+
//right
|
|
198
|
+
if (selected.children && selected.children.length > 0) {
|
|
199
|
+
actions.selectNode(selected.children[0]);
|
|
200
|
+
} else if (selected.displayName === "Columns") {
|
|
201
|
+
const node = query.node(selected.id).get();
|
|
202
|
+
const child = node?.data?.linkedNodes?.Col0;
|
|
203
|
+
if (child) actions.selectNode(child);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (keyCode === 38 && selected.parent) {
|
|
207
|
+
//up
|
|
208
|
+
const prevSib = otherSibling(-1);
|
|
209
|
+
if (prevSib) actions.selectNode(prevSib);
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
}
|
|
212
|
+
if (keyCode === 40 && selected.parent) {
|
|
213
|
+
//down
|
|
214
|
+
const nextSib = otherSibling(1);
|
|
215
|
+
if (nextSib) actions.selectNode(nextSib);
|
|
216
|
+
event.preventDefault();
|
|
162
217
|
}
|
|
163
218
|
}
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
if ((event.ctrlKey || event.metaKey) && event.keyCode == 67) {
|
|
220
|
+
const serialized = JSON.parse(query.serialize());
|
|
221
|
+
const serializedIds = new Set(Object.keys(serialized));
|
|
222
|
+
const currentSelected = query.getEvent("selected");
|
|
223
|
+
const rawSelected = getSelectedNodes(currentSelected);
|
|
224
|
+
if (rawSelected.length === 0 && selected?.id) rawSelected.push(selected.id);
|
|
225
|
+
const selectedNodes = rawSelected
|
|
226
|
+
.map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
|
|
227
|
+
.filter(
|
|
228
|
+
(nodeId) =>
|
|
229
|
+
nodeId && nodeId !== "ROOT" && serializedIds.has(nodeId)
|
|
230
|
+
);
|
|
231
|
+
if (selectedNodes.length === 0) return;
|
|
232
|
+
|
|
233
|
+
if (selectedNodes.length === 1) {
|
|
234
|
+
const { layout } = craftToSaltcorn(
|
|
235
|
+
serialized,
|
|
236
|
+
selectedNodes[0],
|
|
237
|
+
options
|
|
238
|
+
);
|
|
239
|
+
navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
|
|
240
|
+
} else {
|
|
241
|
+
const layouts = selectedNodes.map((nodeId) => {
|
|
242
|
+
const { layout } = craftToSaltcorn(
|
|
243
|
+
serialized,
|
|
244
|
+
nodeId,
|
|
245
|
+
options
|
|
246
|
+
);
|
|
247
|
+
return layout;
|
|
248
|
+
});
|
|
249
|
+
navigator.clipboard.writeText(
|
|
250
|
+
JSON.stringify({ above: layouts }, null, 2)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
185
253
|
}
|
|
186
|
-
if ((event.ctrlKey || event.metaKey) && event.keyCode == 88
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
if ((event.ctrlKey || event.metaKey) && event.keyCode == 88) {
|
|
255
|
+
const serialized = JSON.parse(query.serialize());
|
|
256
|
+
const serializedIds = new Set(Object.keys(serialized));
|
|
257
|
+
const currentSelected = query.getEvent("selected");
|
|
258
|
+
const rawSelected = getSelectedNodes(currentSelected);
|
|
259
|
+
if (rawSelected.length === 0 && selected?.id) rawSelected.push(selected.id);
|
|
260
|
+
const selectedNodes = rawSelected
|
|
261
|
+
.map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
|
|
262
|
+
.filter(
|
|
263
|
+
(nodeId) =>
|
|
264
|
+
nodeId && nodeId !== "ROOT" && serializedIds.has(nodeId)
|
|
265
|
+
);
|
|
266
|
+
if (selectedNodes.length === 0) return;
|
|
267
|
+
|
|
268
|
+
if (selectedNodes.length === 1) {
|
|
269
|
+
const { layout } = craftToSaltcorn(
|
|
270
|
+
serialized,
|
|
271
|
+
selectedNodes[0],
|
|
272
|
+
options
|
|
273
|
+
);
|
|
274
|
+
navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
|
|
275
|
+
actions.delete(selectedNodes[0]);
|
|
276
|
+
} else {
|
|
277
|
+
const layouts = selectedNodes.map((nodeId) => {
|
|
278
|
+
const { layout } = craftToSaltcorn(
|
|
279
|
+
serialized,
|
|
280
|
+
nodeId,
|
|
281
|
+
options
|
|
282
|
+
);
|
|
283
|
+
return layout;
|
|
284
|
+
});
|
|
285
|
+
navigator.clipboard.writeText(
|
|
286
|
+
JSON.stringify({ above: layouts }, null, 2)
|
|
287
|
+
);
|
|
288
|
+
selectedNodes.forEach((nodeId) => actions.delete(nodeId));
|
|
289
|
+
}
|
|
195
290
|
}
|
|
196
291
|
if ((event.ctrlKey || event.metaKey) && event.keyCode == 86) {
|
|
197
|
-
// paste elem from clipboard into container element
|
|
198
|
-
|
|
199
292
|
navigator.clipboard.readText().then((clipText) => {
|
|
200
293
|
const layout = JSON.parse(clipText);
|
|
201
294
|
layoutToNodes(
|
|
@@ -216,6 +309,14 @@ const SettingsPanel = () => {
|
|
|
216
309
|
actions.history.redo();
|
|
217
310
|
}
|
|
218
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
|
+
}
|
|
219
320
|
};
|
|
220
321
|
useEffect(() => {
|
|
221
322
|
window.addEventListener("keydown", handleUserKeyPress);
|
|
@@ -245,7 +346,6 @@ const SettingsPanel = () => {
|
|
|
245
346
|
const siblings = query.node(selected.parent).childNodes();
|
|
246
347
|
const sibIx = siblings.findIndex((sib) => sib === selected.id);
|
|
247
348
|
const elem = recursivelyCloneToElems(query)(selected.id);
|
|
248
|
-
//console.log(elem);
|
|
249
349
|
actions.addNodeTree(
|
|
250
350
|
query.parseReactElement(elem).toNodeTree(),
|
|
251
351
|
parent || "ROOT",
|
|
@@ -255,17 +355,32 @@ const SettingsPanel = () => {
|
|
|
255
355
|
|
|
256
356
|
return (
|
|
257
357
|
<div className="settings-panel card mt-1">
|
|
258
|
-
<div className="card-header px-2 py-1">
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
/>
|
|
265
375
|
)}
|
|
266
376
|
</div>
|
|
267
377
|
<div className="card-body p-2">
|
|
268
|
-
{
|
|
378
|
+
{selectedCount > 1 ? (
|
|
379
|
+
<div>
|
|
380
|
+
<p><strong>{selectedCount} {t("elements selected")}</strong></p>
|
|
381
|
+
<p className="text-muted small">{t("Multi-selection active. Use Shift+Click to add/remove elements.")}</p>
|
|
382
|
+
</div>
|
|
383
|
+
) : selected ? (
|
|
269
384
|
<Fragment>
|
|
270
385
|
{selected.isDeletable && (
|
|
271
386
|
<button
|
|
@@ -273,7 +388,7 @@ const SettingsPanel = () => {
|
|
|
273
388
|
onClick={deleteThis}
|
|
274
389
|
>
|
|
275
390
|
<FontAwesomeIcon icon={faTrashAlt} className="me-1" />
|
|
276
|
-
Delete
|
|
391
|
+
{t("Delete")}
|
|
277
392
|
</button>
|
|
278
393
|
)}
|
|
279
394
|
{hasChildren && !selected.isDeletable ? (
|
|
@@ -282,23 +397,23 @@ const SettingsPanel = () => {
|
|
|
282
397
|
onClick={deleteChildren}
|
|
283
398
|
>
|
|
284
399
|
<FontAwesomeIcon icon={faTrashAlt} className="me-1" />
|
|
285
|
-
Delete contents
|
|
400
|
+
{t("Delete contents")}
|
|
286
401
|
</button>
|
|
287
402
|
) : (
|
|
288
403
|
<button
|
|
289
|
-
title="Duplicate element with its children"
|
|
404
|
+
title={t("Duplicate element with its children")}
|
|
290
405
|
className="btn btn-sm btn-secondary ms-2 duplicate-element-builder"
|
|
291
406
|
onClick={duplicate}
|
|
292
407
|
>
|
|
293
408
|
<FontAwesomeIcon icon={faCopy} className="me-1" />
|
|
294
|
-
Clone
|
|
409
|
+
{t("Clone")}
|
|
295
410
|
</button>
|
|
296
411
|
)}
|
|
297
412
|
<hr className="my-2" />
|
|
298
413
|
{selected.settings && React.createElement(selected.settings)}
|
|
299
414
|
</Fragment>
|
|
300
415
|
) : (
|
|
301
|
-
"No element selected"
|
|
416
|
+
t("No element selected")
|
|
302
417
|
)}
|
|
303
418
|
</div>
|
|
304
419
|
</div>
|
|
@@ -331,7 +446,120 @@ function useWindowDimensions() {
|
|
|
331
446
|
return windowDimensions;
|
|
332
447
|
}
|
|
333
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Custom Layer Component for Craft.js Layers panel
|
|
451
|
+
* Must be defined outside Builder component and memoized to prevent infinite re-renders
|
|
452
|
+
* Added defensive checks for layer properties
|
|
453
|
+
* @category saltcorn-builder
|
|
454
|
+
* @subcategory components
|
|
455
|
+
* @namespace
|
|
456
|
+
*/
|
|
457
|
+
|
|
458
|
+
const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu", "ListColumn"]);
|
|
459
|
+
|
|
460
|
+
const CustomLayerComponent = memo(({ children }) => {
|
|
461
|
+
const {
|
|
462
|
+
id,
|
|
463
|
+
depth,
|
|
464
|
+
expanded,
|
|
465
|
+
hovered,
|
|
466
|
+
actions: { toggleLayer, setExpandedState },
|
|
467
|
+
connectors: { layer, drag, layerHeader },
|
|
468
|
+
} = useLayer((layer) => {
|
|
469
|
+
return {
|
|
470
|
+
hovered: layer?.event?.hovered,
|
|
471
|
+
expanded: layer?.expanded,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const { displayName, hasNodes, isHiddenColumn, selected, connectors: editorConnectors } = useEditor((state) => {
|
|
476
|
+
const node = state.nodes[id];
|
|
477
|
+
const data = node?.data;
|
|
478
|
+
|
|
479
|
+
let name = data?.displayName || data?.name || id;
|
|
480
|
+
if (name === "ROOT" || name === "Canvas") {
|
|
481
|
+
name = data?.name || name;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const nodes = data?.nodes;
|
|
485
|
+
const linkedNodes = data?.linkedNodes;
|
|
486
|
+
const hasChildren = (nodes && nodes.length > 0) || (linkedNodes && Object.keys(linkedNodes).length > 0);
|
|
487
|
+
|
|
488
|
+
// Check if this Column is a linked node of a Card/Container/Tabs/Table
|
|
489
|
+
let shouldHide = false;
|
|
490
|
+
if (name === "Column" && data?.parent) {
|
|
491
|
+
const parentNode = state.nodes[data.parent];
|
|
492
|
+
const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
|
|
493
|
+
if (hiddenColumnParents.has(parentName)) {
|
|
494
|
+
const parentLinked = parentNode?.data?.linkedNodes;
|
|
495
|
+
if (parentLinked && Object.values(parentLinked).includes(id)) {
|
|
496
|
+
shouldHide = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const isSelected = state.events?.selected?.has?.(id) || (state.events?.selected === id);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
displayName: name,
|
|
505
|
+
hasNodes: hasChildren,
|
|
506
|
+
isHiddenColumn: shouldHide,
|
|
507
|
+
selected: isSelected
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const isRoot = id === "ROOT";
|
|
512
|
+
|
|
513
|
+
// Auto-expand hidden linked-node Columns so their children are always
|
|
514
|
+
// visible through the transparent wrapper. Uses setExpandedState(true)
|
|
515
|
+
// instead of toggleLayer() — it's idempotent (no-op when already true),
|
|
516
|
+
// so it won't conflict with craft.js internals or cause toggle loops.
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if ((isHiddenColumn || isRoot) && !expanded) {
|
|
519
|
+
setExpandedState(true);
|
|
520
|
+
}
|
|
521
|
+
}, [isHiddenColumn, isRoot, expanded, setExpandedState]);
|
|
522
|
+
|
|
523
|
+
if (isHiddenColumn || isRoot) {
|
|
524
|
+
return (
|
|
525
|
+
<div
|
|
526
|
+
ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}
|
|
527
|
+
style={{ marginLeft: "-14px" }}
|
|
528
|
+
>
|
|
529
|
+
{children}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}>
|
|
536
|
+
<div
|
|
537
|
+
ref={(dom) => { drag(dom); layerHeader(dom); }}
|
|
538
|
+
className={`builder-layer-node ${hovered ? "hovered" : ""} ${selected ? "selected" : ""}`}
|
|
539
|
+
style={{
|
|
540
|
+
paddingLeft: `${depth * 14 + 10}px`,
|
|
541
|
+
}}
|
|
542
|
+
>
|
|
543
|
+
<span className="layer-name" style={{ flexGrow: 1 }}>{displayName}</span>
|
|
544
|
+
|
|
545
|
+
{hasNodes && (
|
|
546
|
+
<span
|
|
547
|
+
onClick={(e) => {
|
|
548
|
+
e.stopPropagation();
|
|
549
|
+
toggleLayer();
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
<FontAwesomeIcon icon={expanded ? faChevronUp : faChevronDown} fontSize={14} className="float-end fa-lg" />
|
|
553
|
+
</span>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
{children}
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
|
|
334
561
|
const AddColumnButton = () => {
|
|
562
|
+
const { t } = useTranslation();
|
|
335
563
|
const { query, actions } = useEditor(() => {});
|
|
336
564
|
const options = useContext(optionsCtx);
|
|
337
565
|
const addColumn = () => {
|
|
@@ -346,11 +574,43 @@ const AddColumnButton = () => {
|
|
|
346
574
|
onClick={addColumn}
|
|
347
575
|
>
|
|
348
576
|
<FontAwesomeIcon icon={faPlus} className="me-2" />
|
|
349
|
-
Add column
|
|
577
|
+
{t("Add column")}
|
|
350
578
|
</button>
|
|
351
579
|
);
|
|
352
580
|
};
|
|
353
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
|
+
|
|
354
614
|
/**
|
|
355
615
|
* @returns {Fragment}
|
|
356
616
|
* @category saltcorn-builder
|
|
@@ -358,6 +618,7 @@ const AddColumnButton = () => {
|
|
|
358
618
|
* @namespace
|
|
359
619
|
*/
|
|
360
620
|
const HistoryPanel = () => {
|
|
621
|
+
const { t } = useTranslation();
|
|
361
622
|
const { canUndo, canRedo, actions } = useEditor((state, query) => ({
|
|
362
623
|
canUndo: query.history.canUndo(),
|
|
363
624
|
canRedo: query.history.canRedo(),
|
|
@@ -368,7 +629,7 @@ const HistoryPanel = () => {
|
|
|
368
629
|
{canUndo && (
|
|
369
630
|
<button
|
|
370
631
|
className="btn btn-sm btn-secondary ms-2 me-2 undo-builder"
|
|
371
|
-
title="Undo"
|
|
632
|
+
title={t("Undo")}
|
|
372
633
|
onClick={() => actions.history.undo()}
|
|
373
634
|
>
|
|
374
635
|
<FontAwesomeIcon icon={faUndo} />
|
|
@@ -377,7 +638,7 @@ const HistoryPanel = () => {
|
|
|
377
638
|
{canRedo && (
|
|
378
639
|
<button
|
|
379
640
|
className="btn btn-sm btn-secondary redo-builder"
|
|
380
|
-
title="Redo"
|
|
641
|
+
title={t("Redo")}
|
|
381
642
|
onClick={() => actions.history.redo()}
|
|
382
643
|
>
|
|
383
644
|
<FontAwesomeIcon icon={faRedo} />
|
|
@@ -437,7 +698,10 @@ const NextButton = ({ layout }) => {
|
|
|
437
698
|
* @subcategory components
|
|
438
699
|
* @namespace
|
|
439
700
|
*/
|
|
701
|
+
|
|
702
|
+
|
|
440
703
|
const Builder = ({ options, layout, mode }) => {
|
|
704
|
+
const { t } = useTranslation();
|
|
441
705
|
const [showLayers, setShowLayers] = useState(true);
|
|
442
706
|
const [previews, setPreviews] = useState({});
|
|
443
707
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
@@ -446,6 +710,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
446
710
|
const [isEnlarged, setIsEnlarged] = useState(false);
|
|
447
711
|
const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
|
|
448
712
|
const [relationsCache, setRelationsCache] = useState({});
|
|
713
|
+
const [previewDevice, setPreviewDevice] = useState("desktop");
|
|
449
714
|
const { windowWidth, windowHeight } = useWindowDimensions();
|
|
450
715
|
|
|
451
716
|
const [builderHeight, setBuilderHeight] = useState(0);
|
|
@@ -453,21 +718,59 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
453
718
|
|
|
454
719
|
const ref = useRef(null);
|
|
455
720
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (!ref.current) return;
|
|
723
|
+
setBuilderHeight(ref.current.clientHeight);
|
|
724
|
+
const rect = ref.current.getBoundingClientRect();
|
|
725
|
+
setBuilderTop(rect.top);
|
|
726
|
+
});
|
|
462
727
|
|
|
463
728
|
const canvasHeight =
|
|
464
729
|
Math.max(windowHeight - builderTop, builderHeight, 600) - 10;
|
|
465
730
|
return (
|
|
466
731
|
<ErrorBoundary>
|
|
467
|
-
<Editor
|
|
732
|
+
<Editor
|
|
733
|
+
onRender={RenderNode}
|
|
734
|
+
indicator={{
|
|
735
|
+
success: "#28a745",
|
|
736
|
+
thickness: 2,
|
|
737
|
+
className: "builder-drop-indicator",
|
|
738
|
+
}}
|
|
739
|
+
handlers={(store) => new DefaultEventHandlers({
|
|
740
|
+
store,
|
|
741
|
+
isMultiSelectEnabled: (e) => e?.shiftKey || false
|
|
742
|
+
})}
|
|
743
|
+
resolver={{
|
|
744
|
+
Text,
|
|
745
|
+
Empty,
|
|
746
|
+
Columns,
|
|
747
|
+
JoinField,
|
|
748
|
+
Field,
|
|
749
|
+
ViewLink,
|
|
750
|
+
Action,
|
|
751
|
+
HTMLCode,
|
|
752
|
+
LineBreak,
|
|
753
|
+
Aggregation,
|
|
754
|
+
Card,
|
|
755
|
+
Image,
|
|
756
|
+
Link,
|
|
757
|
+
View,
|
|
758
|
+
SearchBar,
|
|
759
|
+
Container,
|
|
760
|
+
Column,
|
|
761
|
+
DropDownFilter,
|
|
762
|
+
DropMenu,
|
|
763
|
+
Tabs,
|
|
764
|
+
Table,
|
|
765
|
+
ToggleFilter,
|
|
766
|
+
ListColumn,
|
|
767
|
+
ListColumns,
|
|
768
|
+
LibraryElem,
|
|
769
|
+
}}
|
|
770
|
+
>
|
|
468
771
|
<Provider value={options}>
|
|
469
772
|
<PreviewCtx.Provider
|
|
470
|
-
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
|
|
773
|
+
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles, previewDevice }}
|
|
471
774
|
>
|
|
472
775
|
<RelationsCtx.Provider
|
|
473
776
|
value={{
|
|
@@ -496,16 +799,16 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
496
799
|
savingState={savingState}
|
|
497
800
|
/>
|
|
498
801
|
<Accordion>
|
|
499
|
-
<div className="card mt-1" accordiontitle="Components">
|
|
802
|
+
<div className="card mt-1" accordiontitle={t("Components")}>
|
|
500
803
|
{{
|
|
501
804
|
show: <ToolboxShow expanded={isLeftEnlarged} />,
|
|
502
805
|
list: <ToolboxList expanded={isLeftEnlarged} />,
|
|
503
806
|
edit: <ToolboxEdit expanded={isLeftEnlarged} />,
|
|
504
807
|
page: <ToolboxPage expanded={isLeftEnlarged} />,
|
|
505
808
|
filter: <ToolboxFilter expanded={isLeftEnlarged} />,
|
|
506
|
-
}[mode] || <div>Missing mode</div>}
|
|
809
|
+
}[mode] || <div>{t("Missing mode")}</div>}
|
|
507
810
|
</div>
|
|
508
|
-
<div accordiontitle="Library">
|
|
811
|
+
<div accordiontitle={t("Library")}>
|
|
509
812
|
<Library expanded={isLeftEnlarged} />
|
|
510
813
|
</div>
|
|
511
814
|
</Accordion>
|
|
@@ -515,7 +818,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
515
818
|
style={isLeftEnlarged ? { width: "13.4rem" } : {}}
|
|
516
819
|
>
|
|
517
820
|
<div className="card-header p-2 d-flex justify-content-between">
|
|
518
|
-
<div>Layers</div>
|
|
821
|
+
<div>{t("Layers")}</div>
|
|
519
822
|
<FontAwesomeIcon
|
|
520
823
|
icon={
|
|
521
824
|
isLeftEnlarged
|
|
@@ -526,12 +829,15 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
526
829
|
"float-end fa-lg builder-expand-toggle-left"
|
|
527
830
|
}
|
|
528
831
|
onClick={() => setIsLeftEnlarged(!isLeftEnlarged)}
|
|
529
|
-
title={isLeftEnlarged ? "Shrink" : "Enlarge"}
|
|
832
|
+
title={isLeftEnlarged ? t("Shrink") : t("Enlarge")}
|
|
530
833
|
/>
|
|
531
834
|
</div>
|
|
532
835
|
{showLayers && (
|
|
533
836
|
<div className="card-body p-0 builder-layers">
|
|
534
|
-
<Layers
|
|
837
|
+
<Layers
|
|
838
|
+
expandRootOnLoad={true}
|
|
839
|
+
renderLayer={CustomLayerComponent}
|
|
840
|
+
/>
|
|
535
841
|
</div>
|
|
536
842
|
)}
|
|
537
843
|
</div>
|
|
@@ -543,75 +849,59 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
543
849
|
options.mode !== "list" ? "emptymsg" : ""
|
|
544
850
|
}`}
|
|
545
851
|
>
|
|
546
|
-
<div>
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
Action,
|
|
556
|
-
HTMLCode,
|
|
557
|
-
LineBreak,
|
|
558
|
-
Aggregation,
|
|
559
|
-
Card,
|
|
560
|
-
Image,
|
|
561
|
-
Link,
|
|
562
|
-
View,
|
|
563
|
-
SearchBar,
|
|
564
|
-
Container,
|
|
565
|
-
Column,
|
|
566
|
-
DropDownFilter,
|
|
567
|
-
DropMenu,
|
|
568
|
-
Tabs,
|
|
569
|
-
Table,
|
|
570
|
-
ToggleFilter,
|
|
571
|
-
ListColumn,
|
|
572
|
-
ListColumns,
|
|
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",
|
|
573
861
|
}}
|
|
574
862
|
>
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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>
|
|
582
872
|
</div>
|
|
583
873
|
</div>
|
|
584
874
|
<div className="col-sm-auto builder-sidebar">
|
|
585
875
|
<div style={{ width: isEnlarged ? "28rem" : "16rem" }}>
|
|
586
|
-
<
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
icon={faSave}
|
|
590
|
-
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
591
|
-
/>
|
|
592
|
-
<FontAwesomeIcon
|
|
593
|
-
icon={faExclamationTriangle}
|
|
594
|
-
color="#ff0033"
|
|
595
|
-
className={savingState.error ? "d-inline" : "d-none"}
|
|
596
|
-
/>
|
|
597
|
-
<FontAwesomeIcon
|
|
598
|
-
icon={
|
|
599
|
-
isEnlarged ? faCaretSquareRight : faCaretSquareLeft
|
|
600
|
-
}
|
|
601
|
-
className={
|
|
602
|
-
"float-end me-2 mt-1 fa-lg builder-expand-toggle-right"
|
|
603
|
-
}
|
|
604
|
-
onClick={() => setIsEnlarged(!isEnlarged)}
|
|
605
|
-
title={isEnlarged ? "Shrink" : "Enlarge"}
|
|
876
|
+
<DevicePreviewToolbar
|
|
877
|
+
previewDevice={previewDevice}
|
|
878
|
+
setPreviewDevice={setPreviewDevice}
|
|
606
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
|
+
)}
|
|
607
897
|
<div
|
|
608
898
|
className={` ${
|
|
609
899
|
savingState.error ? "d-block" : "d-none"
|
|
610
900
|
} my-2 fw-bold`}
|
|
611
901
|
>
|
|
612
|
-
your work is not being saved
|
|
902
|
+
{t("your work is not being saved")}
|
|
613
903
|
</div>
|
|
614
|
-
<SettingsPanel />
|
|
904
|
+
<SettingsPanel isEnlarged={isEnlarged} setIsEnlarged={setIsEnlarged} />
|
|
615
905
|
</div>
|
|
616
906
|
</div>
|
|
617
907
|
</div>
|