@saltcorn/builder 0.5.6-beta.3 → 0.6.0-beta.0
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 +18 -10
- package/dist/builder_bundle.js.map +1 -0
- package/package.json +6 -3
- package/src/components/Builder.js +230 -86
- package/src/components/Library.js +184 -0
- package/src/components/Toolbox.js +130 -69
- package/src/components/elements/Action.js +68 -108
- package/src/components/elements/Aggregation.js +0 -1
- package/src/components/elements/BoxModelEditor.js +374 -0
- package/src/components/elements/Column.js +4 -1
- package/src/components/elements/Columns.js +1 -2
- package/src/components/elements/Container.js +208 -274
- package/src/components/elements/DropDownFilter.js +16 -1
- package/src/components/elements/Field.js +1 -1
- package/src/components/elements/Image.js +92 -33
- package/src/components/elements/Link.js +139 -133
- package/src/components/elements/SearchBar.js +1 -1
- package/src/components/elements/Text.js +60 -26
- package/src/components/elements/ToggleFilter.js +12 -14
- package/src/components/elements/View.js +46 -19
- package/src/components/elements/ViewLink.js +62 -72
- package/src/components/elements/boxmodel.html +253 -0
- package/src/components/elements/utils.js +324 -65
- package/src/components/storage.js +30 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-beta.0",
|
|
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",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"@babel/core": "^7.9.6",
|
|
17
17
|
"@babel/preset-env": "^7.9.6",
|
|
18
18
|
"@babel/preset-react": "^7.9.4",
|
|
19
|
-
"@craftjs/core": "0.1.0-beta.
|
|
20
|
-
"
|
|
19
|
+
"@craftjs/core": "0.1.0-beta.20",
|
|
20
|
+
"saltcorn-craft-layers-noeye": "0.1.0-beta.22",
|
|
21
21
|
"@fonticonpicker/react-fonticonpicker": "^1.2.0",
|
|
22
22
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
|
23
23
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
|
@@ -37,5 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"styled-components": "^4.4.1"
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
Fragment,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { Editor, Frame, Element, Selector, useEditor } from "@craftjs/core";
|
|
3
9
|
import { Text } from "./elements/Text";
|
|
4
10
|
import { Field } from "./elements/Field";
|
|
@@ -29,12 +35,20 @@ import { Link } from "./elements/Link";
|
|
|
29
35
|
import { View } from "./elements/View";
|
|
30
36
|
import { Container } from "./elements/Container";
|
|
31
37
|
import { Column } from "./elements/Column";
|
|
32
|
-
import { Layers } from "
|
|
33
|
-
|
|
38
|
+
import { Layers } from "saltcorn-craft-layers-noeye";
|
|
39
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
40
|
+
import {
|
|
41
|
+
faCopy,
|
|
42
|
+
faUndo,
|
|
43
|
+
faRedo,
|
|
44
|
+
faTrashAlt,
|
|
45
|
+
} from "@fortawesome/free-solid-svg-icons";
|
|
46
|
+
import { Accordion, ErrorBoundary } from "./elements/utils";
|
|
47
|
+
import { InitNewElement, Library } from "./Library";
|
|
34
48
|
const { Provider } = optionsCtx;
|
|
35
49
|
|
|
36
50
|
const SettingsPanel = () => {
|
|
37
|
-
const { actions, selected } = useEditor((state, query) => {
|
|
51
|
+
const { actions, selected, query } = useEditor((state, query) => {
|
|
38
52
|
const currentNodeId = state.events.selected;
|
|
39
53
|
let selected;
|
|
40
54
|
|
|
@@ -42,6 +56,7 @@ const SettingsPanel = () => {
|
|
|
42
56
|
selected = {
|
|
43
57
|
id: currentNodeId,
|
|
44
58
|
name: state.nodes[currentNodeId].data.name,
|
|
59
|
+
parent: state.nodes[currentNodeId].data.parent,
|
|
45
60
|
displayName:
|
|
46
61
|
state.nodes[currentNodeId].data &&
|
|
47
62
|
state.nodes[currentNodeId].data.displayName,
|
|
@@ -59,9 +74,66 @@ const SettingsPanel = () => {
|
|
|
59
74
|
selected,
|
|
60
75
|
};
|
|
61
76
|
});
|
|
77
|
+
|
|
62
78
|
const deleteThis = () => {
|
|
63
79
|
actions.delete(selected.id);
|
|
64
80
|
};
|
|
81
|
+
const otherSibling = (offset) => {
|
|
82
|
+
const siblings = query.node(selected.parent).childNodes();
|
|
83
|
+
const sibIx = siblings.findIndex((sib) => sib === selected.id);
|
|
84
|
+
return siblings[sibIx + offset];
|
|
85
|
+
};
|
|
86
|
+
const handleUserKeyPress = (event) => {
|
|
87
|
+
const { keyCode, target } = event;
|
|
88
|
+
if (target.tagName.toLowerCase() === "body" && selected) {
|
|
89
|
+
//8 backsp, 46 del
|
|
90
|
+
if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
|
|
91
|
+
deleteChildren();
|
|
92
|
+
}
|
|
93
|
+
if (keyCode === 8) {
|
|
94
|
+
//backspace
|
|
95
|
+
const prevSib = otherSibling(-1);
|
|
96
|
+
const parent = selected.parent;
|
|
97
|
+
deleteThis();
|
|
98
|
+
if (prevSib) actions.selectNode(prevSib);
|
|
99
|
+
else actions.selectNode(parent);
|
|
100
|
+
}
|
|
101
|
+
if (keyCode === 46) {
|
|
102
|
+
//del
|
|
103
|
+
const nextSib = otherSibling(1);
|
|
104
|
+
deleteThis();
|
|
105
|
+
if (nextSib) actions.selectNode(nextSib);
|
|
106
|
+
}
|
|
107
|
+
if (keyCode === 37 && selected.parent)
|
|
108
|
+
//left
|
|
109
|
+
actions.selectNode(selected.parent);
|
|
110
|
+
|
|
111
|
+
if (keyCode === 39) {
|
|
112
|
+
//right
|
|
113
|
+
if (selected.children && selected.children.length > 0) {
|
|
114
|
+
actions.selectNode(selected.children[0]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (keyCode === 38 && selected.parent) {
|
|
118
|
+
//up
|
|
119
|
+
const prevSib = otherSibling(-1);
|
|
120
|
+
if (prevSib) actions.selectNode(prevSib);
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
}
|
|
123
|
+
if (keyCode === 40 && selected.parent) {
|
|
124
|
+
//down
|
|
125
|
+
const nextSib = otherSibling(1);
|
|
126
|
+
if (nextSib) actions.selectNode(nextSib);
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
window.addEventListener("keydown", handleUserKeyPress);
|
|
133
|
+
return () => {
|
|
134
|
+
window.removeEventListener("keydown", handleUserKeyPress);
|
|
135
|
+
};
|
|
136
|
+
}, [handleUserKeyPress]);
|
|
65
137
|
const hasChildren =
|
|
66
138
|
selected && selected.children && selected.children.length > 0;
|
|
67
139
|
const deleteChildren = () => {
|
|
@@ -69,28 +141,67 @@ const SettingsPanel = () => {
|
|
|
69
141
|
actions.delete(child);
|
|
70
142
|
});
|
|
71
143
|
};
|
|
144
|
+
const recursivelyCloneToElems = (nodeId, ix) => {
|
|
145
|
+
const {
|
|
146
|
+
data: { type, props, nodes },
|
|
147
|
+
} = query.node(nodeId).get();
|
|
148
|
+
const children = (nodes || []).map(recursivelyCloneToElems);
|
|
149
|
+
return React.createElement(
|
|
150
|
+
type,
|
|
151
|
+
{ ...props, ...(typeof ix !== "undefined" ? { key: ix } : {}) },
|
|
152
|
+
children
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
const duplicate = () => {
|
|
156
|
+
const {
|
|
157
|
+
data: { parent },
|
|
158
|
+
} = query.node(selected.id).get();
|
|
159
|
+
const elem = recursivelyCloneToElems(selected.id);
|
|
160
|
+
actions.addNodeTree(
|
|
161
|
+
query.parseReactElement(elem).toNodeTree(),
|
|
162
|
+
parent || "ROOT"
|
|
163
|
+
);
|
|
164
|
+
};
|
|
72
165
|
return (
|
|
73
|
-
<div className="settings-panel card mt-
|
|
74
|
-
<div className="card-header">
|
|
75
|
-
{selected && selected.displayName
|
|
76
|
-
|
|
77
|
-
|
|
166
|
+
<div className="settings-panel card mt-1">
|
|
167
|
+
<div className="card-header px-2 py-1">
|
|
168
|
+
{selected && selected.displayName ? (
|
|
169
|
+
<Fragment>
|
|
170
|
+
<b>{selected.displayName}</b> settings
|
|
171
|
+
</Fragment>
|
|
172
|
+
) : (
|
|
173
|
+
"Settings"
|
|
174
|
+
)}
|
|
78
175
|
</div>
|
|
79
176
|
<div className="card-body p-2">
|
|
80
177
|
{selected ? (
|
|
81
178
|
<Fragment>
|
|
82
|
-
{}
|
|
83
|
-
{selected.settings && React.createElement(selected.settings)}
|
|
84
179
|
{selected.isDeletable && (
|
|
85
|
-
<button className="btn btn-
|
|
180
|
+
<button className="btn btn-sm btn-danger" onClick={deleteThis}>
|
|
181
|
+
<FontAwesomeIcon icon={faTrashAlt} className="mr-1" />
|
|
86
182
|
Delete
|
|
87
183
|
</button>
|
|
88
184
|
)}
|
|
89
|
-
{hasChildren && !selected.isDeletable
|
|
90
|
-
<button
|
|
185
|
+
{hasChildren && !selected.isDeletable ? (
|
|
186
|
+
<button
|
|
187
|
+
className="btn btn-sm btn-danger"
|
|
188
|
+
onClick={deleteChildren}
|
|
189
|
+
>
|
|
190
|
+
<FontAwesomeIcon icon={faTrashAlt} className="mr-1" />
|
|
91
191
|
Delete contents
|
|
92
192
|
</button>
|
|
193
|
+
) : (
|
|
194
|
+
<button
|
|
195
|
+
title="Duplicate element with its children"
|
|
196
|
+
className="btn btn-sm btn-secondary ml-2"
|
|
197
|
+
onClick={duplicate}
|
|
198
|
+
>
|
|
199
|
+
<FontAwesomeIcon icon={faCopy} className="mr-1" />
|
|
200
|
+
Clone
|
|
201
|
+
</button>
|
|
93
202
|
)}
|
|
203
|
+
<hr className="my-2" />
|
|
204
|
+
{selected.settings && React.createElement(selected.settings)}
|
|
94
205
|
</Fragment>
|
|
95
206
|
) : (
|
|
96
207
|
"No element selected"
|
|
@@ -130,19 +241,47 @@ const ViewPageLink = () => {
|
|
|
130
241
|
const { query, actions } = useEditor(() => {});
|
|
131
242
|
const options = useContext(optionsCtx);
|
|
132
243
|
return options.page_id ? (
|
|
133
|
-
<a
|
|
134
|
-
target="_blank"
|
|
135
|
-
className="d-block mt-2"
|
|
136
|
-
href={`/page/${options.page_name}`}
|
|
137
|
-
>
|
|
244
|
+
<a target="_blank" className="ml-3" href={`/page/${options.page_name}`}>
|
|
138
245
|
View page
|
|
139
246
|
</a>
|
|
140
247
|
) : (
|
|
141
248
|
""
|
|
142
249
|
);
|
|
143
250
|
};
|
|
251
|
+
const HistoryPanel = () => {
|
|
252
|
+
const { canUndo, canRedo, actions } = useEditor((state, query) => ({
|
|
253
|
+
canUndo: query.history.canUndo(),
|
|
254
|
+
canRedo: query.history.canRedo(),
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className="mt-2">
|
|
259
|
+
{canUndo && (
|
|
260
|
+
<button
|
|
261
|
+
className="btn btn-sm btn-secondary mr-2"
|
|
262
|
+
title="Undo"
|
|
263
|
+
onClick={() => actions.history.undo()}
|
|
264
|
+
>
|
|
265
|
+
<FontAwesomeIcon icon={faUndo} />
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
{canRedo && (
|
|
269
|
+
<button
|
|
270
|
+
className="btn btn-sm btn-secondary"
|
|
271
|
+
title="Redo"
|
|
272
|
+
onClick={() => actions.history.redo()}
|
|
273
|
+
>
|
|
274
|
+
<FontAwesomeIcon icon={faRedo} />
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
144
281
|
const NextButton = ({ layout }) => {
|
|
145
282
|
const { query, actions } = useEditor(() => {});
|
|
283
|
+
const options = useContext(optionsCtx);
|
|
284
|
+
|
|
146
285
|
useEffect(() => {
|
|
147
286
|
layoutToNodes(layout, query, actions);
|
|
148
287
|
}, []);
|
|
@@ -158,7 +297,7 @@ const NextButton = ({ layout }) => {
|
|
|
158
297
|
};
|
|
159
298
|
return (
|
|
160
299
|
<button className="btn btn-primary builder-save" onClick={onClick}>
|
|
161
|
-
Next »
|
|
300
|
+
{options.next_button_label || "Next"} »
|
|
162
301
|
</button>
|
|
163
302
|
);
|
|
164
303
|
};
|
|
@@ -166,83 +305,88 @@ const NextButton = ({ layout }) => {
|
|
|
166
305
|
const Builder = ({ options, layout, mode }) => {
|
|
167
306
|
const [showLayers, setShowLayers] = useState(true);
|
|
168
307
|
const [previews, setPreviews] = useState({});
|
|
308
|
+
const nodekeys = useRef([]);
|
|
169
309
|
|
|
170
310
|
return (
|
|
171
|
-
<
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
<div className="
|
|
176
|
-
<div className="
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Frame
|
|
188
|
-
resolver={{
|
|
189
|
-
Text,
|
|
190
|
-
Empty,
|
|
191
|
-
Columns,
|
|
192
|
-
JoinField,
|
|
193
|
-
Field,
|
|
194
|
-
ViewLink,
|
|
195
|
-
Action,
|
|
196
|
-
HTMLCode,
|
|
197
|
-
LineBreak,
|
|
198
|
-
Aggregation,
|
|
199
|
-
Card,
|
|
200
|
-
Image,
|
|
201
|
-
Link,
|
|
202
|
-
View,
|
|
203
|
-
SearchBar,
|
|
204
|
-
Container,
|
|
205
|
-
Column,
|
|
206
|
-
DropDownFilter,
|
|
207
|
-
Tabs,
|
|
208
|
-
ToggleFilter,
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
<Element canvas is={Column}></Element>
|
|
212
|
-
</Frame>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="col-sm-auto builder-sidebar">
|
|
216
|
-
<div style={{ width: "16rem" }}>
|
|
217
|
-
<div className="card">
|
|
218
|
-
<div className="card-header">
|
|
219
|
-
Layers
|
|
220
|
-
<div className="float-right">
|
|
221
|
-
<input
|
|
222
|
-
type="checkbox"
|
|
223
|
-
checked={showLayers}
|
|
224
|
-
onChange={(e) => setShowLayers(e.target.checked)}
|
|
225
|
-
/>
|
|
311
|
+
<ErrorBoundary>
|
|
312
|
+
<Editor>
|
|
313
|
+
<Provider value={options}>
|
|
314
|
+
<PreviewCtx.Provider value={{ previews, setPreviews }}>
|
|
315
|
+
<div className="row" style={{ marginTop: "-5px" }}>
|
|
316
|
+
<div className="col-sm-auto">
|
|
317
|
+
<div className="componets-and-library-accordion toolbox-card">
|
|
318
|
+
<InitNewElement nodekeys={nodekeys} />
|
|
319
|
+
<Accordion>
|
|
320
|
+
<div className="card mt-1" accordiontitle="Components">
|
|
321
|
+
{{
|
|
322
|
+
show: <ToolboxShow />,
|
|
323
|
+
edit: <ToolboxEdit />,
|
|
324
|
+
page: <ToolboxPage />,
|
|
325
|
+
filter: <ToolboxFilter />,
|
|
326
|
+
}[mode] || <div>Missing mode</div>}
|
|
226
327
|
</div>
|
|
227
|
-
|
|
328
|
+
<div accordiontitle="Library">
|
|
329
|
+
<Library />
|
|
330
|
+
</div>
|
|
331
|
+
</Accordion>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="card toolbox-card">
|
|
334
|
+
<div className="card-header">Layers</div>
|
|
228
335
|
{showLayers && (
|
|
229
336
|
<div className="card-body p-0 builder-layers">
|
|
230
337
|
<Layers expandRootOnLoad={true} />
|
|
231
338
|
</div>
|
|
232
339
|
)}
|
|
233
340
|
</div>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
341
|
+
</div>
|
|
342
|
+
<div
|
|
343
|
+
id="builder-main-canvas"
|
|
344
|
+
className={`col builder-mode-${options.mode}`}
|
|
345
|
+
>
|
|
346
|
+
<div>
|
|
347
|
+
<Frame
|
|
348
|
+
resolver={{
|
|
349
|
+
Text,
|
|
350
|
+
Empty,
|
|
351
|
+
Columns,
|
|
352
|
+
JoinField,
|
|
353
|
+
Field,
|
|
354
|
+
ViewLink,
|
|
355
|
+
Action,
|
|
356
|
+
HTMLCode,
|
|
357
|
+
LineBreak,
|
|
358
|
+
Aggregation,
|
|
359
|
+
Card,
|
|
360
|
+
Image,
|
|
361
|
+
Link,
|
|
362
|
+
View,
|
|
363
|
+
SearchBar,
|
|
364
|
+
Container,
|
|
365
|
+
Column,
|
|
366
|
+
DropDownFilter,
|
|
367
|
+
Tabs,
|
|
368
|
+
ToggleFilter,
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
<Element canvas is={Column}></Element>
|
|
372
|
+
</Frame>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
<div className="col-sm-auto builder-sidebar">
|
|
376
|
+
<div style={{ width: "16rem" }}>
|
|
377
|
+
<SaveButton />
|
|
378
|
+
<NextButton layout={layout} />
|
|
379
|
+
<ViewPageLink />
|
|
380
|
+
<HistoryPanel />
|
|
381
|
+
<SettingsPanel />
|
|
382
|
+
</div>
|
|
239
383
|
</div>
|
|
240
384
|
</div>
|
|
241
|
-
</
|
|
242
|
-
</
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</
|
|
385
|
+
</PreviewCtx.Provider>
|
|
386
|
+
</Provider>
|
|
387
|
+
<div className="d-none preview-scratchpad"></div>
|
|
388
|
+
</Editor>
|
|
389
|
+
</ErrorBoundary>
|
|
246
390
|
);
|
|
247
391
|
};
|
|
248
392
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useContext, useState, Fragment } from "react";
|
|
2
|
+
import { useEditor, useNode } from "@craftjs/core";
|
|
3
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
4
|
+
import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
|
|
5
|
+
import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
|
|
6
|
+
import faIcons from "./elements/faicons";
|
|
7
|
+
import { craftToSaltcorn, layoutToNodes } from "./storage";
|
|
8
|
+
import optionsCtx from "./context";
|
|
9
|
+
import { WrapElem } from "./Toolbox";
|
|
10
|
+
|
|
11
|
+
const twoByTwos = (xs) => {
|
|
12
|
+
if (xs.length <= 2) return [xs];
|
|
13
|
+
const [x, y, ...rest] = xs;
|
|
14
|
+
return [[x, y], ...twoByTwos(rest)];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LibraryElem = ({ name, layout }) => {
|
|
18
|
+
const {
|
|
19
|
+
selected,
|
|
20
|
+
connectors: { connect, drag },
|
|
21
|
+
} = useNode((node) => ({ selected: node.events.selected }));
|
|
22
|
+
return (
|
|
23
|
+
<Fragment>
|
|
24
|
+
<span
|
|
25
|
+
className={selected ? "selected-node" : ""}
|
|
26
|
+
ref={(dom) => connect(drag(dom))}
|
|
27
|
+
>
|
|
28
|
+
LibElem
|
|
29
|
+
</span>
|
|
30
|
+
<br />
|
|
31
|
+
</Fragment>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
LibraryElem.craft = {
|
|
36
|
+
displayName: "LibraryElem",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const InitNewElement = ({ nodekeys }) => {
|
|
40
|
+
const { actions, query, connectors } = useEditor((state, query) => {
|
|
41
|
+
return {};
|
|
42
|
+
});
|
|
43
|
+
const onNodesChange = (arg, arg1) => {
|
|
44
|
+
const nodes = arg.getSerializedNodes();
|
|
45
|
+
const newNodeIds = [];
|
|
46
|
+
Object.keys(nodes).forEach((id) => {
|
|
47
|
+
if (!nodekeys.current.includes(id)) {
|
|
48
|
+
newNodeIds.push(id);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
nodekeys.current = Object.keys(nodes);
|
|
52
|
+
if (newNodeIds.length === 1) {
|
|
53
|
+
const id = newNodeIds[0];
|
|
54
|
+
const node = nodes[id];
|
|
55
|
+
if (node.displayName === "LibraryElem") {
|
|
56
|
+
const layout = node.props.layout;
|
|
57
|
+
layoutToNodes(
|
|
58
|
+
layout.layout ? layout.layout : layout,
|
|
59
|
+
query,
|
|
60
|
+
actions,
|
|
61
|
+
node.parent
|
|
62
|
+
);
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
actions.delete(id);
|
|
65
|
+
}, 0);
|
|
66
|
+
} else {
|
|
67
|
+
actions.selectNode(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const nodes = query.getSerializedNodes();
|
|
73
|
+
nodekeys.current = Object.keys(nodes);
|
|
74
|
+
actions.setOptions((options) => {
|
|
75
|
+
const oldf = options.onNodesChange(
|
|
76
|
+
(options.onNodesChange = oldf
|
|
77
|
+
? (q) => {
|
|
78
|
+
oldf(q);
|
|
79
|
+
onNodesChange(q);
|
|
80
|
+
}
|
|
81
|
+
: onNodesChange)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return [];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Library = () => {
|
|
90
|
+
const { actions, selected, query, connectors } = useEditor((state, query) => {
|
|
91
|
+
return {
|
|
92
|
+
selected: state.events.selected,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
const options = useContext(optionsCtx);
|
|
96
|
+
const [adding, setAdding] = useState(false);
|
|
97
|
+
const [newName, setNewName] = useState("");
|
|
98
|
+
const [icon, setIcon] = useState();
|
|
99
|
+
const [recent, setRecent] = useState([]);
|
|
100
|
+
|
|
101
|
+
const addSelected = () => {
|
|
102
|
+
const layout = craftToSaltcorn(JSON.parse(query.serialize()), selected);
|
|
103
|
+
const data = { layout, icon, name: newName };
|
|
104
|
+
fetch(`/library/savefrombuilder`, {
|
|
105
|
+
method: "POST", // or 'PUT'
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"CSRF-Token": options.csrfToken,
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(data),
|
|
111
|
+
});
|
|
112
|
+
setAdding(false);
|
|
113
|
+
setIcon();
|
|
114
|
+
setNewName("");
|
|
115
|
+
setRecent([...recent, data]);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const elemRows = twoByTwos([...(options.library || []), ...recent]);
|
|
119
|
+
return (
|
|
120
|
+
<div className="builder-library">
|
|
121
|
+
<div className="dropdown">
|
|
122
|
+
<button
|
|
123
|
+
className="btn btn-sm btn-secondary dropdown-toggle mt-2"
|
|
124
|
+
type="button"
|
|
125
|
+
id="dropdownMenuButton"
|
|
126
|
+
aria-haspopup="true"
|
|
127
|
+
aria-expanded="false"
|
|
128
|
+
disabled={!selected}
|
|
129
|
+
onClick={() => setAdding(!adding)}
|
|
130
|
+
>
|
|
131
|
+
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
132
|
+
Add
|
|
133
|
+
</button>
|
|
134
|
+
<div
|
|
135
|
+
className={`dropdown-menu py-3 px-4 ${adding ? "show" : ""}`}
|
|
136
|
+
aria-labelledby="dropdownMenuButton"
|
|
137
|
+
>
|
|
138
|
+
<label>Name</label>
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
className="form-control"
|
|
142
|
+
value={newName}
|
|
143
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
144
|
+
/>
|
|
145
|
+
<br />
|
|
146
|
+
<label>Icon</label>
|
|
147
|
+
<FontIconPicker
|
|
148
|
+
className="w-100"
|
|
149
|
+
value={icon}
|
|
150
|
+
icons={faIcons}
|
|
151
|
+
onChange={setIcon}
|
|
152
|
+
isMulti={false}
|
|
153
|
+
/>
|
|
154
|
+
<button className={`btn btn-primary mt-3`} onClick={addSelected}>
|
|
155
|
+
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
156
|
+
Add
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
className={`btn btn-outline-secondary ml-2 mt-3`}
|
|
160
|
+
onClick={() => setAdding(false)}
|
|
161
|
+
>
|
|
162
|
+
<FontAwesomeIcon icon={faTimes} />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="card mt-2">
|
|
167
|
+
{elemRows.map((els, ix) => (
|
|
168
|
+
<div className="toolbar-row" key={ix}>
|
|
169
|
+
{els.map((l, ix) => (
|
|
170
|
+
<WrapElem
|
|
171
|
+
key={ix}
|
|
172
|
+
connectors={connectors}
|
|
173
|
+
icon={l.icon}
|
|
174
|
+
label={l.name}
|
|
175
|
+
>
|
|
176
|
+
<LibraryElem name={l.name} layout={l.layout}></LibraryElem>
|
|
177
|
+
</WrapElem>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|