@saltcorn/builder 0.0.1-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.
Files changed (38) hide show
  1. package/.babelrc +3 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/builder_bundle.js +80 -0
  4. package/package.json +47 -0
  5. package/src/components/Builder.js +477 -0
  6. package/src/components/Library.js +224 -0
  7. package/src/components/RenderNode.js +203 -0
  8. package/src/components/Toolbox.js +688 -0
  9. package/src/components/context.js +9 -0
  10. package/src/components/elements/Action.js +204 -0
  11. package/src/components/elements/Aggregation.js +179 -0
  12. package/src/components/elements/BoxModelEditor.js +398 -0
  13. package/src/components/elements/Card.js +152 -0
  14. package/src/components/elements/Column.js +63 -0
  15. package/src/components/elements/Columns.js +201 -0
  16. package/src/components/elements/Container.js +947 -0
  17. package/src/components/elements/DropDownFilter.js +154 -0
  18. package/src/components/elements/DropMenu.js +156 -0
  19. package/src/components/elements/Empty.js +30 -0
  20. package/src/components/elements/Field.js +239 -0
  21. package/src/components/elements/HTMLCode.js +61 -0
  22. package/src/components/elements/Image.js +320 -0
  23. package/src/components/elements/JoinField.js +206 -0
  24. package/src/components/elements/LineBreak.js +46 -0
  25. package/src/components/elements/Link.js +305 -0
  26. package/src/components/elements/SearchBar.js +141 -0
  27. package/src/components/elements/Tabs.js +347 -0
  28. package/src/components/elements/Text.js +330 -0
  29. package/src/components/elements/ToggleFilter.js +243 -0
  30. package/src/components/elements/View.js +189 -0
  31. package/src/components/elements/ViewLink.js +225 -0
  32. package/src/components/elements/boxmodel.html +253 -0
  33. package/src/components/elements/faicons.js +1643 -0
  34. package/src/components/elements/utils.js +1217 -0
  35. package/src/components/preview_context.js +9 -0
  36. package/src/components/storage.js +506 -0
  37. package/src/index.js +73 -0
  38. package/webpack.config.js +21 -0
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@saltcorn/builder",
3
+ "version": "0.0.1-beta.1",
4
+ "description": "Drag and drop view builder for Saltcorn, open-source no-code platform",
5
+ "main": "index.js",
6
+ "homepage": "https://saltcorn.com",
7
+ "scripts": {
8
+ "build": "webpack --mode production",
9
+ "builddev": "webpack --mode none",
10
+ "test": "echo \"Error: no test specified\"",
11
+ "tsc": "echo \"Error: no TypeScript support yet\"",
12
+ "clean": "echo \"Error: no TypeScript support yet\""
13
+ },
14
+ "repository": "github:saltcorn/saltcorn",
15
+ "author": "Tom Nielsen",
16
+ "license": "MIT",
17
+ "devDependencies": {
18
+ "@babel/core": "7.9.6",
19
+ "@babel/preset-env": "7.9.6",
20
+ "@babel/preset-react": "7.9.4",
21
+ "@craftjs/core": "0.1.0-beta.20",
22
+ "@craftjs/utils": "0.1.0-beta.20",
23
+ "saltcorn-craft-layers-noeye": "0.1.0-beta.22",
24
+ "@fonticonpicker/react-fonticonpicker": "1.2.0",
25
+ "@fortawesome/fontawesome-svg-core": "1.2.34",
26
+ "@fortawesome/free-regular-svg-icons": "5.15.2",
27
+ "@fortawesome/free-solid-svg-icons": "5.15.2",
28
+ "@fortawesome/react-fontawesome": "0.1.14",
29
+ "babel-loader": "8.1.0",
30
+ "ckeditor4-react": "1.4.2",
31
+ "classnames": "2.2.6",
32
+ "prop-types": "15.7.2",
33
+ "react": "16.13.1",
34
+ "react-bootstrap-icons": "1.5.0",
35
+ "react-contenteditable": "3.3.5",
36
+ "react-dom": "16.13.1",
37
+ "react-transition-group": "4.4.1",
38
+ "webpack": "4.43.0",
39
+ "webpack-cli": "3.3.11"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "styled-components": "4.4.1"
46
+ }
47
+ }
@@ -0,0 +1,477 @@
1
+ /**
2
+ * @category saltcorn-builder
3
+ * @module components/Builder
4
+ * @subcategory components
5
+ */
6
+
7
+ import React, {
8
+ useEffect,
9
+ useContext,
10
+ useState,
11
+ Fragment,
12
+ useRef,
13
+ } from "react";
14
+ import { Editor, Frame, Element, Selector, useEditor } from "@craftjs/core";
15
+ import { Text } from "./elements/Text";
16
+ import { Field } from "./elements/Field";
17
+ import { JoinField } from "./elements/JoinField";
18
+ import { Aggregation } from "./elements/Aggregation";
19
+ import { LineBreak } from "./elements/LineBreak";
20
+ import { ViewLink } from "./elements/ViewLink";
21
+ import { Columns } from "./elements/Columns";
22
+ import { SearchBar } from "./elements/SearchBar";
23
+ import { HTMLCode } from "./elements/HTMLCode";
24
+ import { Action } from "./elements/Action";
25
+ import { Image } from "./elements/Image";
26
+ import { Tabs } from "./elements/Tabs";
27
+ import { Empty } from "./elements/Empty";
28
+ import { DropDownFilter } from "./elements/DropDownFilter";
29
+ import { DropMenu } from "./elements/DropMenu";
30
+ import { ToggleFilter } from "./elements/ToggleFilter";
31
+ import optionsCtx from "./context";
32
+ import PreviewCtx from "./preview_context";
33
+ import {
34
+ ToolboxShow,
35
+ ToolboxEdit,
36
+ ToolboxPage,
37
+ ToolboxFilter,
38
+ } from "./Toolbox";
39
+ import { craftToSaltcorn, layoutToNodes } from "./storage";
40
+ import { Card } from "./elements/Card";
41
+ import { Link } from "./elements/Link";
42
+ import { View } from "./elements/View";
43
+ import { Container } from "./elements/Container";
44
+ import { Column } from "./elements/Column";
45
+ import { Layers } from "saltcorn-craft-layers-noeye";
46
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
47
+ import {
48
+ faCopy,
49
+ faUndo,
50
+ faRedo,
51
+ faTrashAlt,
52
+ } from "@fortawesome/free-solid-svg-icons";
53
+ import {
54
+ Accordion,
55
+ ErrorBoundary,
56
+ recursivelyCloneToElems,
57
+ } from "./elements/utils";
58
+ import { InitNewElement, Library } from "./Library";
59
+ import { RenderNode } from "./RenderNode";
60
+ const { Provider } = optionsCtx;
61
+
62
+ /**
63
+ *
64
+ * @returns {div}
65
+ * @category saltcorn-builder
66
+ * @subcategory components
67
+ * @namespace
68
+ */
69
+ const SettingsPanel = () => {
70
+ const { actions, selected, query } = useEditor((state, query) => {
71
+ const currentNodeId = state.events.selected;
72
+ let selected;
73
+
74
+ if (currentNodeId) {
75
+ selected = {
76
+ id: currentNodeId,
77
+ name: state.nodes[currentNodeId].data.name,
78
+ parent: state.nodes[currentNodeId].data.parent,
79
+ displayName:
80
+ state.nodes[currentNodeId].data &&
81
+ state.nodes[currentNodeId].data.displayName,
82
+ settings:
83
+ state.nodes[currentNodeId].related &&
84
+ state.nodes[currentNodeId].related.settings,
85
+ isDeletable: query.node(currentNodeId).isDeletable(),
86
+ children:
87
+ state.nodes[currentNodeId].data &&
88
+ state.nodes[currentNodeId].data.nodes,
89
+ };
90
+ }
91
+
92
+ return {
93
+ selected,
94
+ };
95
+ });
96
+
97
+ /** */
98
+ const deleteThis = () => {
99
+ actions.delete(selected.id);
100
+ };
101
+
102
+ /**
103
+ * @param {number} offset
104
+ * @returns {NodeId}
105
+ */
106
+ const otherSibling = (offset) => {
107
+ const siblings = query.node(selected.parent).childNodes();
108
+ const sibIx = siblings.findIndex((sib) => sib === selected.id);
109
+ return siblings[sibIx + offset];
110
+ };
111
+
112
+ /**
113
+ * @param {object} event
114
+ */
115
+ const handleUserKeyPress = (event) => {
116
+ const { keyCode, target } = event;
117
+ if (target.tagName.toLowerCase() === "body" && selected) {
118
+ //8 backsp, 46 del
119
+ if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
120
+ deleteChildren();
121
+ }
122
+ if (keyCode === 8) {
123
+ //backspace
124
+ const prevSib = otherSibling(-1);
125
+ const parent = selected.parent;
126
+ deleteThis();
127
+ if (prevSib) actions.selectNode(prevSib);
128
+ else actions.selectNode(parent);
129
+ }
130
+ if (keyCode === 46) {
131
+ //del
132
+ const nextSib = otherSibling(1);
133
+ deleteThis();
134
+ if (nextSib) actions.selectNode(nextSib);
135
+ }
136
+ if (keyCode === 37 && selected.parent)
137
+ //left
138
+ actions.selectNode(selected.parent);
139
+
140
+ if (keyCode === 39) {
141
+ //right
142
+ if (selected.children && selected.children.length > 0) {
143
+ actions.selectNode(selected.children[0]);
144
+ } else if (selected.displayName === "Columns") {
145
+ const node = query.node(selected.id).get();
146
+ const child = node?.data?.linkedNodes?.Col0;
147
+ if (child) actions.selectNode(child);
148
+ }
149
+ }
150
+ if (keyCode === 38 && selected.parent) {
151
+ //up
152
+ const prevSib = otherSibling(-1);
153
+ if (prevSib) actions.selectNode(prevSib);
154
+ event.preventDefault();
155
+ }
156
+ if (keyCode === 40 && selected.parent) {
157
+ //down
158
+ const nextSib = otherSibling(1);
159
+ if (nextSib) actions.selectNode(nextSib);
160
+ event.preventDefault();
161
+ }
162
+ }
163
+ };
164
+ useEffect(() => {
165
+ window.addEventListener("keydown", handleUserKeyPress);
166
+ return () => {
167
+ window.removeEventListener("keydown", handleUserKeyPress);
168
+ };
169
+ }, [handleUserKeyPress]);
170
+ const hasChildren =
171
+ selected && selected.children && selected.children.length > 0;
172
+
173
+ /**
174
+ * @returns {void}
175
+ */
176
+ const deleteChildren = () => {
177
+ selected.children.forEach((child) => {
178
+ actions.delete(child);
179
+ });
180
+ };
181
+
182
+ /**
183
+ * @returns {void}
184
+ */
185
+ const duplicate = () => {
186
+ const {
187
+ data: { parent },
188
+ } = query.node(selected.id).get();
189
+ const siblings = query.node(selected.parent).childNodes();
190
+ const sibIx = siblings.findIndex((sib) => sib === selected.id);
191
+ const elem = recursivelyCloneToElems(query)(selected.id);
192
+ //console.log(elem);
193
+ actions.addNodeTree(
194
+ query.parseReactElement(elem).toNodeTree(),
195
+ parent || "ROOT",
196
+ sibIx + 1
197
+ );
198
+ };
199
+ return (
200
+ <div className="settings-panel card mt-1">
201
+ <div className="card-header px-2 py-1">
202
+ {selected && selected.displayName ? (
203
+ <Fragment>
204
+ <b>{selected.displayName}</b> settings
205
+ </Fragment>
206
+ ) : (
207
+ "Settings"
208
+ )}
209
+ </div>
210
+ <div className="card-body p-2">
211
+ {selected ? (
212
+ <Fragment>
213
+ {selected.isDeletable && (
214
+ <button className="btn btn-sm btn-danger" onClick={deleteThis}>
215
+ <FontAwesomeIcon icon={faTrashAlt} className="me-1" />
216
+ Delete
217
+ </button>
218
+ )}
219
+ {hasChildren && !selected.isDeletable ? (
220
+ <button
221
+ className="btn btn-sm btn-danger"
222
+ onClick={deleteChildren}
223
+ >
224
+ <FontAwesomeIcon icon={faTrashAlt} className="me-1" />
225
+ Delete contents
226
+ </button>
227
+ ) : (
228
+ <button
229
+ title="Duplicate element with its children"
230
+ className="btn btn-sm btn-secondary ms-2"
231
+ onClick={duplicate}
232
+ >
233
+ <FontAwesomeIcon icon={faCopy} className="me-1" />
234
+ Clone
235
+ </button>
236
+ )}
237
+ <hr className="my-2" />
238
+ {selected.settings && React.createElement(selected.settings)}
239
+ </Fragment>
240
+ ) : (
241
+ "No element selected"
242
+ )}
243
+ </div>
244
+ </div>
245
+ );
246
+ };
247
+
248
+ /**
249
+ * @returns {button}
250
+ * @category saltcorn-builder
251
+ * @subcategory components
252
+ * @namespace
253
+ */
254
+ const SaveButton = () => {
255
+ const { query, actions } = useEditor(() => {});
256
+ const options = useContext(optionsCtx);
257
+
258
+ /**
259
+ * @returns {void}
260
+ */
261
+ const onClick = () => {
262
+ const data = craftToSaltcorn(JSON.parse(query.serialize()));
263
+ const urlroot = options.page_id ? "pageedit" : "viewedit";
264
+ fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
265
+ method: "POST", // or 'PUT'
266
+ headers: {
267
+ "Content-Type": "application/json",
268
+ "CSRF-Token": options.csrfToken,
269
+ },
270
+ body: JSON.stringify(data),
271
+ });
272
+ };
273
+ return options.page_id || options.view_id ? (
274
+ <button
275
+ className="btn btn-sm btn-outline-secondary me-2 builder-save-ajax"
276
+ onClick={onClick}
277
+ >
278
+ Save
279
+ </button>
280
+ ) : (
281
+ ""
282
+ );
283
+ };
284
+
285
+ /**
286
+ * @returns {a|""}
287
+ * @category saltcorn-builder
288
+ * @subcategory components
289
+ * @namespace
290
+ */
291
+ const ViewPageLink = () => {
292
+ const { query, actions } = useEditor(() => {});
293
+ const options = useContext(optionsCtx);
294
+ return options.page_id ? (
295
+ <a target="_blank" className="d-block" href={`/page/${options.page_name}`}>
296
+ View page in new tab &raquo;
297
+ </a>
298
+ ) : (
299
+ ""
300
+ );
301
+ };
302
+
303
+ /**
304
+ * @returns {Fragment}
305
+ * @category saltcorn-builder
306
+ * @subcategory components
307
+ * @namespace
308
+ */
309
+ const HistoryPanel = () => {
310
+ const { canUndo, canRedo, actions } = useEditor((state, query) => ({
311
+ canUndo: query.history.canUndo(),
312
+ canRedo: query.history.canRedo(),
313
+ }));
314
+
315
+ return (
316
+ <Fragment>
317
+ {canUndo && (
318
+ <button
319
+ className="btn btn-sm btn-secondary ms-2 me-2"
320
+ title="Undo"
321
+ onClick={() => actions.history.undo()}
322
+ >
323
+ <FontAwesomeIcon icon={faUndo} />
324
+ </button>
325
+ )}
326
+ {canRedo && (
327
+ <button
328
+ className="btn btn-sm btn-secondary"
329
+ title="Redo"
330
+ onClick={() => actions.history.redo()}
331
+ >
332
+ <FontAwesomeIcon icon={faRedo} />
333
+ </button>
334
+ )}
335
+ </Fragment>
336
+ );
337
+ };
338
+
339
+ /**
340
+ * @param {object} opts
341
+ * @param {object} opts.layout
342
+ * @returns {button}
343
+ * @category saltcorn-builder
344
+ * @subcategory components
345
+ * @namespace
346
+ */
347
+ const NextButton = ({ layout }) => {
348
+ const { query, actions } = useEditor(() => {});
349
+ const options = useContext(optionsCtx);
350
+
351
+ useEffect(() => {
352
+ layoutToNodes(layout, query, actions);
353
+ }, []);
354
+
355
+ /**
356
+ * @returns {void}
357
+ */
358
+ const onClick = () => {
359
+ const { columns, layout } = craftToSaltcorn(JSON.parse(query.serialize()));
360
+ document
361
+ .querySelector("form#scbuildform input[name=columns]")
362
+ .setAttribute("value", encodeURIComponent(JSON.stringify(columns)));
363
+ document
364
+ .querySelector("form#scbuildform input[name=layout]")
365
+ .setAttribute("value", encodeURIComponent(JSON.stringify(layout)));
366
+ document.getElementById("scbuildform").submit();
367
+ };
368
+ return (
369
+ <button className="btn btn-sm btn-primary builder-save" onClick={onClick}>
370
+ {options.next_button_label || "Next"} &raquo;
371
+ </button>
372
+ );
373
+ };
374
+
375
+ /**
376
+ * @param {object} props
377
+ * @param {object} props.options
378
+ * @param {object} props.layout
379
+ * @param {string} props.mode
380
+ * @returns {ErrorBoundary}
381
+ * @category saltcorn-builder
382
+ * @subcategory components
383
+ * @namespace
384
+ */
385
+ const Builder = ({ options, layout, mode }) => {
386
+ const [showLayers, setShowLayers] = useState(true);
387
+ const [previews, setPreviews] = useState({});
388
+ const [uploadedFiles, setUploadedFiles] = useState([]);
389
+ const nodekeys = useRef([]);
390
+
391
+ return (
392
+ <ErrorBoundary>
393
+ <Editor onRender={RenderNode}>
394
+ <Provider value={options}>
395
+ <PreviewCtx.Provider
396
+ value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
397
+ >
398
+ <div className="row" style={{ marginTop: "-5px" }}>
399
+ <div className="col-sm-auto left-builder-col">
400
+ <div className="componets-and-library-accordion toolbox-card">
401
+ <InitNewElement nodekeys={nodekeys} />
402
+ <Accordion>
403
+ <div className="card mt-1" accordiontitle="Components">
404
+ {{
405
+ show: <ToolboxShow />,
406
+ edit: <ToolboxEdit />,
407
+ page: <ToolboxPage />,
408
+ filter: <ToolboxFilter />,
409
+ }[mode] || <div>Missing mode</div>}
410
+ </div>
411
+ <div accordiontitle="Library">
412
+ <Library />
413
+ </div>
414
+ </Accordion>
415
+ </div>
416
+ <div className="card toolbox-card pe-0">
417
+ <div className="card-header">Layers</div>
418
+ {showLayers && (
419
+ <div className="card-body p-0 builder-layers">
420
+ <Layers expandRootOnLoad={true} />
421
+ </div>
422
+ )}
423
+ </div>
424
+ </div>
425
+ <div
426
+ id="builder-main-canvas"
427
+ className={`col builder-mode-${options.mode}`}
428
+ >
429
+ <div>
430
+ <Frame
431
+ resolver={{
432
+ Text,
433
+ Empty,
434
+ Columns,
435
+ JoinField,
436
+ Field,
437
+ ViewLink,
438
+ Action,
439
+ HTMLCode,
440
+ LineBreak,
441
+ Aggregation,
442
+ Card,
443
+ Image,
444
+ Link,
445
+ View,
446
+ SearchBar,
447
+ Container,
448
+ Column,
449
+ DropDownFilter,
450
+ DropMenu,
451
+ Tabs,
452
+ ToggleFilter,
453
+ }}
454
+ >
455
+ <Element canvas is={Column}></Element>
456
+ </Frame>
457
+ </div>
458
+ </div>
459
+ <div className="col-sm-auto builder-sidebar">
460
+ <div style={{ width: "16rem" }}>
461
+ <SaveButton />
462
+ <NextButton layout={layout} />
463
+ <HistoryPanel />
464
+ <ViewPageLink />
465
+ <SettingsPanel />
466
+ </div>
467
+ </div>
468
+ </div>
469
+ </PreviewCtx.Provider>
470
+ </Provider>
471
+ <div className="d-none preview-scratchpad"></div>
472
+ </Editor>
473
+ </ErrorBoundary>
474
+ );
475
+ };
476
+
477
+ export default Builder;