@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.5.6-beta.3",
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.17",
20
- "@craftjs/layers": "0.1.0-beta.17",
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, { useEffect, useContext, useState, Fragment } from "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 "@craftjs/layers";
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-2">
74
- <div className="card-header">
75
- {selected && selected.displayName
76
- ? `Settings: ${selected.displayName}`
77
- : "Settings"}
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-danger mt-2" onClick={deleteThis}>
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 className="btn btn-danger mt-2" onClick={deleteChildren}>
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 &raquo;
300
+ {options.next_button_label || "Next"} &raquo;
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
- <Editor>
172
- <Provider value={options}>
173
- <PreviewCtx.Provider value={{ previews, setPreviews }}>
174
- <div className="row">
175
- <div className="col-sm-auto">
176
- <div className="card">
177
- {{
178
- show: <ToolboxShow />,
179
- edit: <ToolboxEdit />,
180
- page: <ToolboxPage />,
181
- filter: <ToolboxFilter />,
182
- }[mode] || <div>Missing mode</div>}
183
- </div>
184
- </div>
185
- <div id="builder-main-canvas" className="col">
186
- <div>
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
- </div>
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
- <SettingsPanel />
235
- <br />
236
- <SaveButton />
237
- <NextButton layout={layout} />
238
- <ViewPageLink />
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
- </div>
242
- </PreviewCtx.Provider>
243
- </Provider>
244
- <div className="d-none preview-scratchpad"></div>
245
- </Editor>
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
+ };