@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
@@ -0,0 +1,1217 @@
1
+ /**
2
+ * @category saltcorn-builder
3
+ * @module components/elements/utils
4
+ * @subcategory components / elements
5
+ */
6
+
7
+ import React, { Fragment, useContext, useState } from "react";
8
+ import optionsCtx from "../context";
9
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
10
+ import {
11
+ faChevronDown,
12
+ faChevronRight,
13
+ } from "@fortawesome/free-solid-svg-icons";
14
+ import { useNode, Element } from "@craftjs/core";
15
+ import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
16
+ import faIcons from "./faicons";
17
+ import { Columns, ntimes } from "./Columns";
18
+
19
+ export const DynamicFontAwesomeIcon = ({ icon, className }) => {
20
+ if (!icon) return null;
21
+ return <i className={`${icon} ${className || ""}`}></i>;
22
+ };
23
+
24
+ export /**
25
+ * @param {boolean} is_block
26
+ * @returns {object}
27
+ */
28
+ const blockProps = (is_block) =>
29
+ is_block ? { style: { display: "block" } } : {};
30
+
31
+ export /**
32
+ * @param {object} props
33
+ * @param {boolean} props.block
34
+ * @param {function} props.setProp
35
+ * @returns {div}
36
+ * @category saltcorn-builder
37
+ * @subcategory components / elements / utils
38
+ * @namespace
39
+ */
40
+ const BlockSetting = ({ block, setProp }) => (
41
+ <div className="form-check">
42
+ <input
43
+ className="form-check-input"
44
+ name="block"
45
+ type="checkbox"
46
+ checked={block}
47
+ onChange={(e) => {
48
+ if (e.target) {
49
+ const target_value = e.target.checked;
50
+ setProp((prop) => (prop.block = target_value));
51
+ }
52
+ }}
53
+ />
54
+ <label className="form-check-label">Block display</label>
55
+ </div>
56
+ );
57
+
58
+ export const BlockOrInlineSetting = ({ block, inline, textStyle, setProp }) =>
59
+ !textStyle || !textStyle.startsWith("h") ? (
60
+ <BlockSetting block={block} setProp={setProp} />
61
+ ) : (
62
+ <div className="form-check">
63
+ <input
64
+ className="form-check-input"
65
+ name="inline"
66
+ type="checkbox"
67
+ checked={inline}
68
+ onChange={(e) => {
69
+ if (e.target) {
70
+ const target_value = e.target.checked;
71
+ setProp((prop) => (prop.inline = target_value));
72
+ }
73
+ }}
74
+ />
75
+ <label className="form-check-label">Inline display</label>
76
+ </div>
77
+ );
78
+
79
+ export /**
80
+ * @param {object} props
81
+ * @param {function} props.setProp
82
+ * @param {object} props.isFormula
83
+ * @param {object} props.node
84
+ * @param {string} props.nodekey
85
+ * @param {string} props.children
86
+ * @returns {Fragment}
87
+ * @namespace
88
+ * @category saltcorn-builder
89
+ * @subcategory components / elements / utils
90
+ */
91
+ const OrFormula = ({ setProp, isFormula, node, nodekey, children }) => {
92
+ const { mode } = useContext(optionsCtx);
93
+
94
+ /**
95
+ * @returns {void}
96
+ */
97
+ const switchIsFml = () => {
98
+ const isFmlAfter = !isFormula[nodekey];
99
+ setProp((prop) => {
100
+ prop.isFormula[nodekey] = isFmlAfter;
101
+ if (isFmlAfter && prop[nodekey] && prop[nodekey][0] !== '"') {
102
+ prop[nodekey] = `"${prop[nodekey]}"`;
103
+ } else if (
104
+ !isFmlAfter &&
105
+ typeof prop[nodekey] === "string" &&
106
+ prop[nodekey][0] === '"' &&
107
+ prop[nodekey][prop[nodekey].length - 1] === '"'
108
+ ) {
109
+ prop[nodekey] = prop[nodekey].substring(1, prop[nodekey].length - 1);
110
+ }
111
+ });
112
+ };
113
+ return mode !== "show" ? (
114
+ children
115
+ ) : (
116
+ <Fragment>
117
+ <div className="input-group input-group-sm w-100">
118
+ {isFormula[nodekey] ? (
119
+ <input
120
+ type="text"
121
+ className="form-control text-to-display"
122
+ value={node[nodekey]}
123
+ onChange={(e) => {
124
+ if (e.target) {
125
+ const target_value = e.target.value;
126
+ setProp((prop) => (prop[nodekey] = target_value));
127
+ }
128
+ }}
129
+ />
130
+ ) : (
131
+ children
132
+ )}
133
+
134
+ <button
135
+ className={`btn activate-formula ${
136
+ isFormula[nodekey] ? "btn-secondary" : "btn-outline-secondary"
137
+ }`}
138
+ title="Calculated formula"
139
+ type="button"
140
+ onClick={switchIsFml}
141
+ >
142
+ <i className="fas fa-calculator"></i>
143
+ </button>
144
+ </div>
145
+ {isFormula[nodekey] && (
146
+ <div style={{ marginTop: "-5px" }}>
147
+ <small className="text-muted font-monospace">FORMULA</small>
148
+ </div>
149
+ )}
150
+ </Fragment>
151
+ );
152
+ };
153
+
154
+ export /**
155
+ * @param {object} props
156
+ * @param {string} props.minRole
157
+ * @param {function} props.setProp
158
+ * @returns {div}
159
+ * @namespace
160
+ * @category saltcorn-builder
161
+ * @subcategory components / elements / utils
162
+ */
163
+ const MinRoleSetting = ({ minRole, setProp }) => {
164
+ const options = useContext(optionsCtx);
165
+ return (
166
+ <div>
167
+ <label>Minimum Role</label>
168
+ <select
169
+ className="form-control form-select"
170
+ value={minRole}
171
+ onChange={(e) => (e) => {
172
+ if (e.target) {
173
+ const target_value = e.target.value;
174
+ setProp((prop) => (prop.minRole = target_value));
175
+ }
176
+ }}
177
+ >
178
+ {options.roles.map((r) => (
179
+ <option key={r.id} value={r.id}>
180
+ {r.role}
181
+ </option>
182
+ ))}
183
+ </select>
184
+ </div>
185
+ );
186
+ };
187
+
188
+ export /**
189
+ * @param {object} props
190
+ * @param {string} props.minRole
191
+ * @param {function} props.setProp
192
+ * @returns {tr}
193
+ * @namespace
194
+ * @category saltcorn-builder
195
+ * @subcategory components / elements / utils
196
+ */
197
+ const MinRoleSettingRow = ({ minRole, setProp }) => {
198
+ const options = useContext(optionsCtx);
199
+ return (
200
+ <tr>
201
+ <td>
202
+ <label>Minimum Role</label>
203
+ </td>
204
+ <td>
205
+ <select
206
+ value={minRole}
207
+ className="form-control form-select"
208
+ onChange={(e) => {
209
+ if (e.target) {
210
+ const target_value = e.target.value;
211
+ setProp((prop) => (prop.minRole = target_value));
212
+ }
213
+ }}
214
+ >
215
+ {options.roles.map((r) => (
216
+ <option key={r.id} value={r.id}>
217
+ {r.role}
218
+ </option>
219
+ ))}
220
+ </select>
221
+ </td>
222
+ </tr>
223
+ );
224
+ };
225
+
226
+ /**
227
+ * @param {object} props
228
+ * @param {string} props.textStyle
229
+ * @param {function} props.setProp
230
+ * @returns {select}
231
+ * @namespace
232
+ * @category saltcorn-builder
233
+ * @subcategory components / elements / utils
234
+ */
235
+ const TextStyleSelect = ({ textStyle, setProp }) => {
236
+ return (
237
+ <select
238
+ value={textStyle}
239
+ className="form-control form-select"
240
+ onChange={(e) => {
241
+ if (e.target) {
242
+ const target_value = e.target.value;
243
+ setProp((prop) => (prop.textStyle = target_value));
244
+ }
245
+ }}
246
+ >
247
+ <option value="">Normal</option>
248
+ <option value="h1">Heading 1</option>
249
+ <option value="h2">Heading 2</option>
250
+ <option value="h3">Heading 3</option>
251
+ <option value="h4">Heading 4</option>
252
+ <option value="h5">Heading 5</option>
253
+ <option value="h6">Heading 6</option>
254
+ <option value="fw-bold">Bold</option>
255
+ <option value="fst-italic">Italics</option>
256
+ <option value="small">Small</option>
257
+ <option value="text-muted">Muted</option>
258
+ <option value="text-underline">Underline</option>
259
+ <option value="font-monospace">Monospace</option>
260
+ </select>
261
+ );
262
+ };
263
+
264
+ export /**
265
+ * @param {object} props
266
+ * @param {string} props.textStyle
267
+ * @param {function} props.setProp
268
+ * @returns {div}
269
+ * @namespace
270
+ * @category saltcorn-builder
271
+ * @subcategory components / elements / utils
272
+ */
273
+ const TextStyleSetting = ({ textStyle, setProp }) => {
274
+ return (
275
+ <div>
276
+ <label>Text Style</label>
277
+ <TextStyleSelect textStyle={textStyle} setProp={setProp} />
278
+ </div>
279
+ );
280
+ };
281
+
282
+ export /**
283
+ * @param {object} props
284
+ * @param {string} props.textStyle
285
+ * @param {function} props.setProp
286
+ * @returns {tr}
287
+ * @namespace
288
+ * @category saltcorn-builder
289
+ * @subcategory components / elements / utils
290
+ */
291
+ const TextStyleRow = ({ textStyle, setProp }) => {
292
+ return (
293
+ <tr>
294
+ <td>
295
+ <label>Text Style</label>
296
+ </td>
297
+ <td>
298
+ <TextStyleSelect textStyle={textStyle} setProp={setProp} />
299
+ </td>
300
+ </tr>
301
+ );
302
+ };
303
+
304
+ export /**
305
+ * @param {object} props
306
+ * @param {string[]} [props.titles]
307
+ * @param {object[]} props.children
308
+ * @returns {Fragment}
309
+ * @category saltcorn-builder
310
+ * @subcategory components / elements / utils
311
+ * @namespace
312
+ */
313
+ const Accordion = ({ titles, children }) => {
314
+ const [currentTab, setCurrentTab] = useState(0);
315
+ return (
316
+ <Fragment>
317
+ {children.map((child, ix) => {
318
+ const isCurrent = ix === currentTab;
319
+ return (
320
+ <Fragment key={ix}>
321
+ <div
322
+ className={`bg-${
323
+ isCurrent ? "primary" : "secondary"
324
+ } ps-1 text-white w-100 mt-1`}
325
+ onClick={() => setCurrentTab(ix)}
326
+ >
327
+ <span className="w-1em">
328
+ {isCurrent ? (
329
+ <FontAwesomeIcon icon={faChevronDown} />
330
+ ) : (
331
+ <FontAwesomeIcon icon={faChevronRight} />
332
+ )}
333
+ </span>
334
+ {child.props.accordiontitle || titles[ix]}
335
+ </div>
336
+ {isCurrent ? child : null}
337
+ </Fragment>
338
+ );
339
+ })}
340
+ </Fragment>
341
+ );
342
+ };
343
+
344
+ /**
345
+ * @param {object} opts
346
+ * @param {string} opts.url
347
+ * @param {object} opts.body
348
+ * @param {object} opts.options
349
+ * @param {function} opts.setPreviews
350
+ * @param {*} opts.node_id
351
+ * @param {boolean} opts.isView
352
+ * @returns {void}
353
+ */
354
+ const fetchPreview = ({ url, body, options, setPreviews, node_id, isView }) => {
355
+ fetch(url, {
356
+ method: "POST",
357
+ headers: {
358
+ "Content-Type": "application/json",
359
+ "CSRF-Token": options.csrfToken,
360
+ },
361
+ body: JSON.stringify(body),
362
+ })
363
+ .then(function (response) {
364
+ if (response.status < 399) return response.text();
365
+ else return "";
366
+ })
367
+ .then(function (html) {
368
+ $(".preview-scratchpad").html(html);
369
+ $(".preview-scratchpad").find("a").attr("href", "#");
370
+ $(".preview-scratchpad")
371
+ .find("[onclick], button, a, input, select")
372
+ .attr("onclick", "return false");
373
+
374
+ //.attr("disabled", true);
375
+ $(".preview-scratchpad").find("textarea").attr("disabled", true);
376
+ $(".preview-scratchpad .full-page-width").removeClass("full-page-width");
377
+ if (isView) {
378
+ $(".preview-scratchpad").find("input").attr("readonly", true);
379
+ }
380
+ const newHtml = $(".preview-scratchpad").html();
381
+ setPreviews((prevState) => ({ ...prevState, [node_id]: newHtml }));
382
+ });
383
+ };
384
+
385
+ /**
386
+ * @function
387
+ * @param {object} [args = {}]
388
+ * @return {function}
389
+ */
390
+ export const fetchFieldPreview =
391
+ (args = {}) =>
392
+ (changes = {}) => {
393
+ const { node_id, options, name, fieldview, setPreviews } = {
394
+ ...args,
395
+ ...changes,
396
+ };
397
+ const configuration = {
398
+ ...(args.configuration || {}),
399
+ ...(changes.configuration || {}),
400
+ };
401
+ fetchPreview({
402
+ options,
403
+ node_id,
404
+ setPreviews,
405
+ url: `/field/preview/${options.tableName}/${name}/${fieldview}`,
406
+ body: { configuration },
407
+ });
408
+ };
409
+
410
+ /**
411
+ * @function
412
+ * @param {object} [args = {}]
413
+ * @return {function}
414
+ */
415
+ export const fetchViewPreview =
416
+ (args = {}) =>
417
+ (changes = {}) => {
418
+ const { node_id, options, view, setPreviews, configuration } = {
419
+ ...args,
420
+ ...changes,
421
+ };
422
+ let viewname,
423
+ body = configuration ? { ...configuration } : {};
424
+ if (view.includes(":")) {
425
+ const [reltype, rest] = view.split(":");
426
+ const [vnm] = rest.split(".");
427
+ viewname = vnm;
428
+ body.reltype = reltype;
429
+ body.path = rest;
430
+ } else viewname = view;
431
+
432
+ fetchPreview({
433
+ options,
434
+ node_id,
435
+ setPreviews,
436
+ url: `/view/${viewname}/preview`,
437
+ body,
438
+ isView: true,
439
+ });
440
+ };
441
+
442
+ export /**
443
+ * @param {object} props
444
+ * @param {boolean} props.vert
445
+ * @param {string} [props.autoable]
446
+ * @param {...*} props.props
447
+ * @returns {select}
448
+ * @category saltcorn-builder
449
+ * @subcategory components / elements / utils
450
+ * @namespace
451
+ */
452
+ const SelectUnits = ({ vert, autoable, ...props }) => (
453
+ <select {...props}>
454
+ <option>px</option>
455
+ <option>%</option>
456
+ <option>{vert ? "vh" : "vw"}</option>
457
+ <option>em</option>
458
+ <option>rem</option>
459
+ {autoable && <option>auto</option>}
460
+ </select>
461
+ );
462
+
463
+ /**
464
+ * @function
465
+ * @param {string} [styles]
466
+ * @returns {string}
467
+ */
468
+ export const parseStyles = (styles) =>
469
+ (styles || "")
470
+ .split("\n")
471
+ .join("")
472
+ .split(";")
473
+ .filter((style) => style.split(":")[0] && style.split(":")[1])
474
+ .map((style) => [
475
+ style
476
+ .split(":")[0]
477
+ .trim()
478
+ .replace(/-./g, (c) => c.substr(1).toUpperCase()),
479
+ style.split(":")[1].trim(),
480
+ ])
481
+ .reduce(
482
+ (styleObj, style) => ({
483
+ ...styleObj,
484
+ [style[0]]: style[1],
485
+ }),
486
+ {}
487
+ );
488
+
489
+ /**
490
+ * @function
491
+ * @param {object} styles
492
+ * @returns {object}
493
+ */
494
+ export const reactifyStyles = (styles) => {
495
+ const toCamel = (s) => {
496
+ return s.replace(/([-][a-z])/gi, ($1) => {
497
+ return $1.toUpperCase().replace("-", "");
498
+ });
499
+ };
500
+ const reactified = {};
501
+ Object.keys(styles).forEach((k) => {
502
+ reactified[toCamel(k)] = styles[k];
503
+ });
504
+ return reactified;
505
+ };
506
+
507
+ /**
508
+ * @param {object} f
509
+ * @returns {boolean}
510
+ */
511
+ const isCheckbox = (f) =>
512
+ f && f.type && (f.type === "Bool" || f.type.name === "Bool");
513
+
514
+ /**
515
+ * @function
516
+ * @param {function} setProp
517
+ * @param {*} fieldview
518
+ * @param {object[]} [fields]
519
+ * @returns {void}
520
+ */
521
+ export const setInitialConfig = (setProp, fieldview, fields) => {
522
+ (fields || []).forEach((f, ix) => {
523
+ if (f.input_type === "select")
524
+ setProp((prop) => {
525
+ if (!prop.configuration[f.name])
526
+ prop.configuration[f.name] = f.options[0] || "";
527
+ });
528
+ });
529
+ };
530
+
531
+ /**
532
+ * @param {object} props
533
+ * @param {string} props.value
534
+ * @param {function} props.onChange
535
+ * @category saltcorn-builder
536
+ * @subcategory components / elements / utils
537
+ * @namespace
538
+ * @returns {input|button}
539
+ */
540
+ const ColorInput = ({ value, onChange }) =>
541
+ value ? (
542
+ <input
543
+ type="color"
544
+ value={value}
545
+ className="form-control"
546
+ onChange={(e) => e.target && onChange(e.target.value)}
547
+ />
548
+ ) : (
549
+ <button
550
+ className="btn btn-sm btn-outline-secondary"
551
+ onClick={() => onChange("#000000")}
552
+ >
553
+ <small>Set color</small>
554
+ </button>
555
+ );
556
+
557
+ export /**
558
+ * @param {object} props
559
+ * @param {object[]} props.fields
560
+ * @param {object} props.configuration
561
+ * @param {function} props.setProp
562
+ * @param {object} props.node
563
+ * @param {function} props.onChange
564
+ * @returns {div}
565
+ * @category saltcorn-builder
566
+ * @subcategory components / elements / utils
567
+ * @namespace
568
+ */
569
+ const ConfigForm = ({ fields, configuration, setProp, node, onChange }) => (
570
+ <div>
571
+ {fields.map((f, ix) => {
572
+ if (f.showIf && configuration) {
573
+ let noshow = false;
574
+ Object.entries(f.showIf).forEach(([nm, value]) => {
575
+ if (Array.isArray(value))
576
+ noshow = noshow || value.includes(configuration[nm]);
577
+ else noshow = noshow || value !== configuration[nm];
578
+ });
579
+ if (noshow) return null;
580
+ }
581
+ return (
582
+ <div key={ix}>
583
+ {!isCheckbox(f) ? <label>{f.label || f.name}</label> : null}
584
+ <ConfigField
585
+ field={f}
586
+ configuration={configuration}
587
+ setProp={setProp}
588
+ onChange={onChange}
589
+ />
590
+ {f.sublabel ? (
591
+ <i dangerouslySetInnerHTML={{ __html: f.sublabel }}></i>
592
+ ) : null}
593
+ </div>
594
+ );
595
+ })}
596
+ <br />
597
+ </div>
598
+ );
599
+
600
+ /**
601
+ * @param {object|undefined} x
602
+ * @param {object} y
603
+ * @returns {object}
604
+ */
605
+ const or_if_undef = (x, y) => (typeof x === "undefined" ? y : x);
606
+
607
+ export /**
608
+ * @param {object} props
609
+ * @param {object} props.field
610
+ * @param {object} [props.configuration]
611
+ * @param {function} props.setProp
612
+ * @param {function} props.onChange
613
+ * @param {object} props.props
614
+ * @param {boolean} props.isStyle
615
+ * @returns {select|input}
616
+ * @category saltcorn-builder
617
+ * @subcategory components / elements / utils
618
+ * @namespace
619
+ */
620
+ const ConfigField = ({
621
+ field,
622
+ configuration,
623
+ setProp,
624
+ onChange,
625
+ props,
626
+ isStyle,
627
+ }) => {
628
+ /**
629
+ * @param {object} v
630
+ * @returns {void}
631
+ */
632
+ const options = useContext(optionsCtx);
633
+
634
+ const myOnChange = (v) => {
635
+ setProp((prop) => {
636
+ if (configuration) {
637
+ if (!prop.configuration) prop.configuration = {};
638
+ prop.configuration[field.name] = v;
639
+ } else if (isStyle) {
640
+ if (!prop.style) prop.style = {};
641
+ prop.style[field.name] = v;
642
+ } else prop[field.name] = v;
643
+ });
644
+ onChange && onChange(field.name, v);
645
+ };
646
+ const value = or_if_undef(
647
+ configuration
648
+ ? configuration[field.name]
649
+ : isStyle
650
+ ? props.style[field.name]
651
+ : props[field.name],
652
+ field.default
653
+ );
654
+ if (field.input_type === "fromtype") field.input_type = null;
655
+ if (field.type && field.type.name === "String" && field.attributes.options) {
656
+ field.input_type = "select";
657
+ field.options =
658
+ typeof field.attributes.options === "string"
659
+ ? field.attributes.options.split(",").map((s) => s.trim())
660
+ : field.attributes.options;
661
+ if (!field.required && field.options) field.options.unshift("");
662
+ }
663
+ const dispatch = {
664
+ String() {
665
+ if (field.attributes?.options) {
666
+ const options =
667
+ typeof field.attributes.options === "string"
668
+ ? field.attributes.options.split(",").map((s) => s.trim())
669
+ : field.attributes.options;
670
+ return (
671
+ <select
672
+ className="form-control form-select"
673
+ value={value || ""}
674
+ onChange={(e) => e.target && myOnChange(e.target.value)}
675
+ >
676
+ {options.map((o, ix) => (
677
+ <option
678
+ key={ix}
679
+ value={typeof o === "string" ? o : o.value || o.name}
680
+ >
681
+ {typeof o === "string" ? o : o.label}
682
+ </option>
683
+ ))}
684
+ </select>
685
+ );
686
+ } else
687
+ return (
688
+ <input
689
+ type="text"
690
+ className="form-control"
691
+ value={value || ""}
692
+ onChange={(e) => e.target && myOnChange(e.target.value)}
693
+ />
694
+ );
695
+ },
696
+ Font: () => (
697
+ <select
698
+ className="form-control form-select"
699
+ value={value || ""}
700
+ onChange={(e) => e.target && myOnChange(e.target.value)}
701
+ >
702
+ <option value={""}></option>
703
+ {Object.entries(options.fonts || {}).map(([nm, ff], ix) => (
704
+ <option key={ix} value={ff}>
705
+ {nm}
706
+ </option>
707
+ ))}
708
+ </select>
709
+ ),
710
+ Integer: () => (
711
+ <input
712
+ type="number"
713
+ className="form-control"
714
+ step={field.step || 1}
715
+ min={field.min}
716
+ max={field.max}
717
+ value={value || ""}
718
+ onChange={(e) => e.target && myOnChange(e.target.value)}
719
+ />
720
+ ),
721
+ Float: () => (
722
+ <input
723
+ type="number"
724
+ className="form-control"
725
+ value={value || ""}
726
+ step={0.01}
727
+ onChange={(e) => e.target && myOnChange(e.target.value)}
728
+ />
729
+ ),
730
+ Color: () => <ColorInput value={value} onChange={(c) => myOnChange(c)} />,
731
+ Bool: () => (
732
+ <div className="form-check">
733
+ <input
734
+ type="checkbox"
735
+ className="form-check-input"
736
+ checked={value}
737
+ onChange={(e) => e.target && myOnChange(e.target.checked)}
738
+ />
739
+ <label className="form-check-label">{field.label}</label>
740
+ </div>
741
+ ),
742
+ textarea: () => (
743
+ <textarea
744
+ rows="6"
745
+ type="text"
746
+ className="form-control"
747
+ value={value}
748
+ onChange={(e) => e.target && myOnChange(e.target.value)}
749
+ />
750
+ ),
751
+ code: () => (
752
+ <textarea
753
+ rows="6"
754
+ type="text"
755
+ className="form-control"
756
+ value={value}
757
+ onChange={(e) => e.target && myOnChange(e.target.value)}
758
+ />
759
+ ),
760
+ select: () => (
761
+ <select
762
+ className="form-control form-select"
763
+ value={value || ""}
764
+ onChange={(e) => e.target && myOnChange(e.target.value)}
765
+ >
766
+ {field.options.map((o, ix) => (
767
+ <option key={ix}>{o}</option>
768
+ ))}
769
+ </select>
770
+ ),
771
+ btn_select: () => (
772
+ <div className="btn-group w-100" role="group">
773
+ {field.options.map((o, ix) => (
774
+ <button
775
+ key={ix}
776
+ title={o.title || o.value}
777
+ type="button"
778
+ style={{ width: `${Math.floor(100 / field.options.length)}%` }}
779
+ className={`btn btn-sm btn-${
780
+ value !== o.value ? "outline-" : ""
781
+ }secondary ${field.btnClass || ""}`}
782
+ onClick={() => myOnChange(o.value)}
783
+ >
784
+ {o.label}
785
+ </button>
786
+ ))}
787
+ </div>
788
+ ),
789
+ DimUnits: () => {
790
+ let styleVal, styleDim;
791
+ if (isStyle && value === "auto") {
792
+ styleVal = "";
793
+ styleDim = "auto";
794
+ } else if (isStyle && value && typeof value === "string") {
795
+ const matches = value.match(/^([0-9]+\.?[0-9]*)(.*)/);
796
+ if (matches) {
797
+ styleVal = matches[1];
798
+ styleDim = matches[2];
799
+ }
800
+ }
801
+ return (
802
+ <Fragment>
803
+ {styleDim !== "auto" && (
804
+ <input
805
+ type="number"
806
+ value={(isStyle ? styleVal : value) || ""}
807
+ className="w-50 form-control-sm d-inline dimunit"
808
+ disabled={field.autoable && styleDim === "auto"}
809
+ onChange={(e) =>
810
+ myOnChange(
811
+ isStyle
812
+ ? `${e.target.value}${styleDim || "px"}`
813
+ : e.target.value
814
+ )
815
+ }
816
+ />
817
+ )}
818
+ <SelectUnits
819
+ value={or_if_undef(
820
+ configuration
821
+ ? configuration[field.name + "Unit"]
822
+ : isStyle
823
+ ? styleDim
824
+ : props[field.name + "Unit"],
825
+ "px"
826
+ )}
827
+ autoable={field.autoable}
828
+ className={`w-${
829
+ styleDim === "auto" ? 100 : 50
830
+ } form-control-sm d-inline dimunit`}
831
+ vert={true}
832
+ onChange={(e) => {
833
+ if (!e.target) return;
834
+ const target_value = e.target.value;
835
+ setProp((prop) => {
836
+ const myStyleVal =
837
+ target_value === "auto" && field.autoable && isStyle
838
+ ? ""
839
+ : styleVal;
840
+ if (configuration)
841
+ prop.configuration[field.name + "Unit"] = target_value;
842
+ else if (isStyle) {
843
+ prop.style[field.name] = `${or_if_undef(
844
+ myStyleVal,
845
+ 0
846
+ )}${target_value}`;
847
+ } else prop[field.name + "Unit"] = target_value;
848
+ });
849
+ }}
850
+ />
851
+ </Fragment>
852
+ );
853
+ },
854
+ };
855
+ const f = dispatch[field.input_type || field.type.name || field.type];
856
+ return f ? f() : null;
857
+ };
858
+
859
+ export /**
860
+ * @param {object[]} fields
861
+ * @category saltcorn-builder
862
+ * @subcategory components / elements / utils
863
+ * @namespace
864
+ * @returns {table}
865
+ */
866
+ const SettingsFromFields = (fields) => () => {
867
+ const node = useNode((node) => {
868
+ const ps = {};
869
+ fields.forEach((f) => {
870
+ ps[f.name] = node.data.props[f.name];
871
+ });
872
+ if (fields.some((f) => f.canBeFormula))
873
+ ps.isFormula = node.data.props.isFormula;
874
+ return ps;
875
+ });
876
+ const {
877
+ actions: { setProp },
878
+ } = node;
879
+
880
+ return (
881
+ <table className="w-100">
882
+ <tbody>
883
+ {fields.map((f, ix) => (
884
+ <SettingsRow field={f} key={ix} node={node} setProp={setProp} />
885
+ ))}
886
+ </tbody>
887
+ </table>
888
+ );
889
+ };
890
+
891
+ export /**
892
+ * @param {object} props
893
+ * @param {string} props.title
894
+ * @returns {tr}
895
+ * @category saltcorn-builder
896
+ * @subcategory components / elements / utils
897
+ * @namespace
898
+ */
899
+ const SettingsSectionHeaderRow = ({ title }) => (
900
+ <tr>
901
+ <th colSpan="2">{title}</th>
902
+ </tr>
903
+ );
904
+
905
+ export /**
906
+ * @param {object} props
907
+ * @param {string} props.field
908
+ * @param {object} props.node
909
+ * @param {function} props.setProp
910
+ * @param {function} props.onChange
911
+ * @param {boolean} props.isStyle
912
+ * @returns {tr}
913
+ * @category saltcorn-builder
914
+ * @subcategory components / elements / utils
915
+ * @namespace
916
+ */
917
+ const SettingsRow = ({ field, node, setProp, onChange, isStyle }) => {
918
+ const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
919
+ const needLabel = field.type !== "Bool";
920
+ const inner = field.canBeFormula ? (
921
+ <OrFormula
922
+ nodekey={field.name}
923
+ isFormula={node.isFormula}
924
+ {...{ setProp, node }}
925
+ >
926
+ <ConfigField
927
+ field={field}
928
+ props={node}
929
+ setProp={setProp}
930
+ onChange={onChange}
931
+ />
932
+ </OrFormula>
933
+ ) : (
934
+ <ConfigField
935
+ field={field}
936
+ props={node}
937
+ setProp={setProp}
938
+ onChange={onChange}
939
+ isStyle={isStyle}
940
+ />
941
+ );
942
+ return (
943
+ <tr>
944
+ {fullWidth ? (
945
+ <td colSpan="2">
946
+ {needLabel && <label>{field.label}</label>}
947
+ {inner}
948
+ </td>
949
+ ) : (
950
+ <Fragment>
951
+ <td>
952
+ <label>{field.label}</label>
953
+ </td>
954
+ <td>{inner}</td>
955
+ </Fragment>
956
+ )}
957
+ </tr>
958
+ );
959
+ };
960
+
961
+ /**
962
+ * @category saltcorn-builder
963
+ * @extends React.Component
964
+ */
965
+ export class ErrorBoundary extends React.Component {
966
+ /**
967
+ * ErrorBoundary constructor
968
+ * @param {object} props
969
+ */
970
+ constructor(props) {
971
+ super(props);
972
+ this.state = { hasError: false, reported: false };
973
+ }
974
+
975
+ /**
976
+ * @param {*} error
977
+ * @returns {object}
978
+ */
979
+ static getDerivedStateFromError(error) {
980
+ // Update state so the next render will show the fallback UI.
981
+ return { hasError: true };
982
+ }
983
+
984
+ /**
985
+ * @param {object} error
986
+ * @param {object} errorInfo
987
+ * @returns {void}
988
+ */
989
+ componentDidCatch(error, errorInfo) {
990
+ // You can also log the error to an error reporting service
991
+ //logErrorToMyService(error, errorInfo);
992
+ console.log(
993
+ "ErrorBoundary reporting: ",
994
+ JSON.stringify(error),
995
+ JSON.stringify(errorInfo)
996
+ );
997
+
998
+ if (!this.state.reported) {
999
+ const data = {
1000
+ message: error.message,
1001
+ stack: (error && error.stack) || "",
1002
+ };
1003
+
1004
+ fetch("/crashlog/", {
1005
+ method: "POST",
1006
+ headers: {
1007
+ "Content-Type": "application/json",
1008
+ "CSRF-Token": _sc_globalCsrf,
1009
+ },
1010
+ body: JSON.stringify(data),
1011
+ });
1012
+ this.setState({ reported: true });
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * @returns {object}
1018
+ */
1019
+ render() {
1020
+ return this.props.children;
1021
+ }
1022
+ }
1023
+
1024
+ export /**
1025
+ * @param {object} props
1026
+ * @param {function} props.setProp
1027
+ * @param {string} [props.btnClass = null]
1028
+ * @param {string} [props.keyPrefix = ""]
1029
+ * @param {object} props.values
1030
+ * @param {boolean} [props.linkFirst = false]
1031
+ * @returns {tr}
1032
+ * @category saltcorn-builder
1033
+ * @subcategory components / elements / utils
1034
+ * @namespace
1035
+ */
1036
+ const ButtonOrLinkSettingsRows = ({
1037
+ setProp,
1038
+ btnClass = null,
1039
+ keyPrefix = "",
1040
+ values,
1041
+ linkFirst = false,
1042
+ }) => {
1043
+ const setAProp = (key) => (e) => {
1044
+ if (e.target) {
1045
+ const target_value = e.target.value;
1046
+ setProp((prop) => (prop[key] = target_value));
1047
+ }
1048
+ };
1049
+ const addBtnClass = (s) => (btnClass ? `${btnClass} ${s}` : s);
1050
+ return [
1051
+ <tr key="btnstyle">
1052
+ <td>
1053
+ <label>Style</label>
1054
+ </td>
1055
+ <td>
1056
+ <select
1057
+ className="form-control form-select"
1058
+ value={values[keyPrefix + "style"]}
1059
+ onChange={setAProp(keyPrefix + "style")}
1060
+ >
1061
+ {linkFirst ? (
1062
+ <option value={addBtnClass("btn-link")}>Link</option>
1063
+ ) : null}
1064
+ <option value={addBtnClass("btn-primary")}>Primary button</option>
1065
+ <option value={addBtnClass("btn-secondary")}>Secondary button</option>
1066
+ <option value={addBtnClass("btn-success")}>Success button</option>
1067
+ <option value={addBtnClass("btn-danger")}>Danger button</option>
1068
+ <option value={addBtnClass("btn-outline-primary")}>
1069
+ Primary outline button
1070
+ </option>
1071
+ <option value={addBtnClass("btn-outline-secondary")}>
1072
+ Secondary outline button
1073
+ </option>
1074
+ <option value={addBtnClass("btn-custom-color")}>
1075
+ Button custom color
1076
+ </option>
1077
+ {!linkFirst ? (
1078
+ <option value={addBtnClass("btn-link")}>Link</option>
1079
+ ) : null}
1080
+ </select>
1081
+ </td>
1082
+ </tr>,
1083
+ <tr key="btnsz">
1084
+ <td>
1085
+ <label>Size</label>
1086
+ </td>
1087
+ <td>
1088
+ <select
1089
+ className="form-control form-select"
1090
+ value={values[keyPrefix + "size"]}
1091
+ onChange={setAProp(keyPrefix + "size")}
1092
+ >
1093
+ <option value="">Standard</option>
1094
+ <option value="btn-lg">Large</option>
1095
+ <option value="btn-sm">Small</option>
1096
+ <option value="btn-block">Block</option>
1097
+ <option value="btn-block btn-lg">Large block</option>
1098
+ </select>
1099
+ </td>
1100
+ </tr>,
1101
+ <tr key="btnicon">
1102
+ <td>
1103
+ <label>Icon</label>
1104
+ </td>
1105
+ <td>
1106
+ <FontIconPicker
1107
+ value={values[keyPrefix + "icon"]}
1108
+ onChange={(value) =>
1109
+ setProp((prop) => (prop[keyPrefix + "icon"] = value))
1110
+ }
1111
+ isMulti={false}
1112
+ icons={faIcons}
1113
+ />
1114
+ </td>
1115
+ </tr>,
1116
+ ...(values[keyPrefix + "style"] === addBtnClass("btn-custom-color")
1117
+ ? [
1118
+ <tr key="btnbgcol">
1119
+ <td>
1120
+ <label>Background</label>
1121
+ </td>
1122
+ <td>
1123
+ <input
1124
+ type="color"
1125
+ value={values[keyPrefix + "bgcol"]}
1126
+ className="form-control-sm w-50"
1127
+ onChange={setAProp(keyPrefix + "bgcol")}
1128
+ />
1129
+ </td>
1130
+ </tr>,
1131
+ <tr key="btnbdcol">
1132
+ <td>
1133
+ <label>Border</label>
1134
+ </td>
1135
+ <td>
1136
+ <input
1137
+ type="color"
1138
+ value={values[keyPrefix + "bordercol"]}
1139
+ className="form-control-sm w-50"
1140
+ onChange={setAProp(keyPrefix + "bordercol")}
1141
+ />
1142
+ </td>
1143
+ </tr>,
1144
+ <tr key="btntxtcol">
1145
+ <td>
1146
+ <label>Text</label>
1147
+ </td>
1148
+ <td>
1149
+ <input
1150
+ type="color"
1151
+ value={values[keyPrefix + "textcol"]}
1152
+ className="form-control-sm w-50"
1153
+ onChange={setAProp(keyPrefix + "textcol")}
1154
+ />
1155
+ </td>
1156
+ </tr>,
1157
+ ]
1158
+ : []),
1159
+ ];
1160
+ };
1161
+
1162
+ /**
1163
+ * @function
1164
+ * @param {string} style
1165
+ * @returns {object}
1166
+ */
1167
+ export const bstyleopt = (style) => ({
1168
+ value: style,
1169
+ title: style,
1170
+ label: (
1171
+ <div
1172
+ style={{
1173
+ borderLeftStyle: style,
1174
+ borderTopStyle: style,
1175
+ height: "15px",
1176
+ width: "6px",
1177
+ }}
1178
+ ></div>
1179
+ ),
1180
+ });
1181
+ export const recursivelyCloneToElems = (query) => (nodeId, ix) => {
1182
+ const { data } = query.node(nodeId).get();
1183
+ const { type, props, nodes } = data;
1184
+ const children = (nodes || []).map(recursivelyCloneToElems(query));
1185
+ if (data.displayName === "Columns") {
1186
+ const cols = ntimes(data.props.ncols, (ix) =>
1187
+ recursivelyCloneToElems(query)(data.linkedNodes["Col" + ix])
1188
+ );
1189
+ return React.createElement(Columns, {
1190
+ ...props,
1191
+ ...(typeof ix !== "undefined" ? { key: ix } : {}),
1192
+ contents: cols,
1193
+ });
1194
+ }
1195
+ if (data.isCanvas)
1196
+ return React.createElement(
1197
+ Element,
1198
+ {
1199
+ ...props,
1200
+ canvas: true,
1201
+ is: type,
1202
+ ...(typeof ix !== "undefined" ? { key: ix } : {}),
1203
+ },
1204
+ children
1205
+ );
1206
+ return React.createElement(
1207
+ type,
1208
+ {
1209
+ ...props,
1210
+ ...(typeof ix !== "undefined" ? { key: ix } : {}),
1211
+ },
1212
+ children
1213
+ );
1214
+ };
1215
+
1216
+ export const isBlock = (block, inline, textStyle) =>
1217
+ !textStyle || !textStyle.startsWith("h") ? block : !inline;