@saltcorn/builder 1.6.0-alpha.8 → 1.6.0-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.
@@ -0,0 +1,285 @@
1
+
2
+ import React, { useState, useEffect } from "react";
3
+ import { useEditor } from "@craftjs/core";
4
+ import { useLayer } from "@craftjs/layers";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { faChevronDown, faChevronUp, faArrowUp, faArrowDown, faLevelUpAlt } from "@fortawesome/free-solid-svg-icons";
7
+
8
+ const CustomLayer = ({ children }) => {
9
+ const [isEditing, setIsEditing] = useState(false);
10
+ const [editValue, setEditValue] = useState("");
11
+ const [isMouseOver, setIsMouseOver] = useState(false);
12
+
13
+ const {
14
+ id,
15
+ depth,
16
+ expanded,
17
+ actions: { toggleLayer, setExpandedState },
18
+ connectors: { layer, drag, layerHeader },
19
+ } = useLayer((layer) => ({
20
+ expanded: layer?.expanded,
21
+ }));
22
+
23
+ const {
24
+ displayName,
25
+ hasNodes,
26
+ isHiddenColumn,
27
+ selected,
28
+ parentId,
29
+ childIndex,
30
+ siblingCount,
31
+ canMoveOut,
32
+ connectors: editorConnectors,
33
+ actions: editorActions,
34
+ query,
35
+ } = useEditor((state) => {
36
+ const node = state.nodes[id];
37
+ const data = node?.data;
38
+
39
+ let name =
40
+ data?.custom?.displayName ||
41
+ data?.props?.custom?.displayName ||
42
+ data?.displayName ||
43
+ data?.name ||
44
+ id;
45
+
46
+ if (name === "ROOT" || name === "Canvas") {
47
+ name = data?.name || name;
48
+ }
49
+
50
+ // Rename linked Columns for Tabs and Table
51
+ if (name === "Column" && data?.parent) {
52
+ const parentNode = state.nodes[data.parent];
53
+ const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
54
+ const parentLinked = parentNode?.data?.linkedNodes;
55
+ if (parentLinked) {
56
+ const key = Object.keys(parentLinked).find(k => parentLinked[k] === id);
57
+ if (key) {
58
+ if (parentName === "Tabs") {
59
+ const index = parseInt(key.replace("Tab", ""), 10);
60
+ if (!isNaN(index)) {
61
+ name = `Tab ${index + 1}`;
62
+ }
63
+ } else if (parentName === "Table") {
64
+ const match = key.match(/^cell_(\d+)_(\d+)$/);
65
+ if (match) {
66
+ name = `R${parseInt(match[1], 10) + 1}C${parseInt(match[2], 10) + 1}`;
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ const nodes = data?.nodes;
74
+ const linkedNodes = data?.linkedNodes;
75
+ const hasChildren =
76
+ (nodes && nodes.length > 0) ||
77
+ (linkedNodes && Object.keys(linkedNodes).length > 0);
78
+
79
+ // Hide the ROOT Column and linked Columns of Card/Container
80
+ let shouldHide = false;
81
+ if (id === "ROOT") {
82
+ shouldHide = true;
83
+ } else if ((data?.displayName === "Column" || data?.name === "Column") && data?.parent) {
84
+ const parentNode = state.nodes[data.parent];
85
+ const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
86
+ if (parentName === "Card" || parentName === "Container") {
87
+ const parentLinked = parentNode?.data?.linkedNodes;
88
+ if (parentLinked && Object.values(parentLinked).includes(id)) {
89
+ shouldHide = true;
90
+ }
91
+ }
92
+ }
93
+
94
+ const isSelected =
95
+ state.events?.selected?.has?.(id) || state.events?.selected === id;
96
+
97
+ const parent = data?.parent;
98
+ let childIx = -1;
99
+ let sibCount = 0;
100
+ if (parent && state.nodes[parent]) {
101
+ const parentNodes = state.nodes[parent]?.data?.nodes || [];
102
+ childIx = parentNodes.indexOf(id);
103
+ sibCount = parentNodes.length;
104
+ }
105
+
106
+ let moveOut = false;
107
+ if (parent && state.nodes[parent]) {
108
+ const grandparent = state.nodes[parent]?.data?.parent;
109
+ if (grandparent && grandparent !== "ROOT" && state.nodes[grandparent]) {
110
+ const greatGrandparent = state.nodes[grandparent]?.data?.parent;
111
+ if (greatGrandparent && state.nodes[greatGrandparent]) {
112
+ moveOut = true;
113
+ }
114
+ }
115
+ }
116
+
117
+ return {
118
+ displayName: name,
119
+ hasNodes: hasChildren,
120
+ isHiddenColumn: shouldHide,
121
+ selected: isSelected,
122
+ parentId: parent,
123
+ childIndex: childIx,
124
+ siblingCount: sibCount,
125
+ canMoveOut: moveOut,
126
+ };
127
+ });
128
+
129
+ // Keep hidden columns always expanded so their children are visible
130
+ useEffect(() => {
131
+ if (isHiddenColumn && !expanded) {
132
+ setExpandedState(true);
133
+ }
134
+ }, [isHiddenColumn, expanded, setExpandedState]);
135
+
136
+ return (
137
+ <div
138
+ ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}
139
+ style={isHiddenColumn ? { marginLeft: "-10px" } : undefined}
140
+ >
141
+ <div
142
+ ref={(dom) => { drag(dom); layerHeader(dom); }}
143
+ className={`builder-layer-node ${isMouseOver ? "hovered" : ""} ${selected ? "selected" : ""}`}
144
+ style={{
145
+ cursor: isHiddenColumn ? "default" : "pointer",
146
+ display: isHiddenColumn ? "none" : "flex",
147
+ alignItems: "center",
148
+ padding: `4px 4px 4px ${depth * 10 + 6}px`,
149
+ overflow: "hidden",
150
+ }}
151
+ onClick={() => editorActions.selectNode(id)}
152
+ onMouseEnter={() => setIsMouseOver(true)}
153
+ onMouseLeave={() => setIsMouseOver(false)}
154
+ >
155
+
156
+ {isEditing ? (
157
+ <input
158
+ value={editValue}
159
+ onChange={(e) => setEditValue(e.target.value)}
160
+ onBlur={() => {
161
+ const trimmed = editValue.trim();
162
+ editorActions.setCustom(id, (custom) => {
163
+ if (trimmed && trimmed !== (custom.displayName || "")) {
164
+ custom.displayName = trimmed;
165
+ } else if (!trimmed) {
166
+ delete custom.displayName;
167
+ }
168
+ });
169
+ setIsEditing(false);
170
+ }}
171
+ onKeyDown={(e) => {
172
+ if (e.key === "Enter") e.target.blur();
173
+ if (e.key === "Escape") setIsEditing(false);
174
+ }}
175
+ onClick={(e) => e.stopPropagation()}
176
+ onMouseDown={(e) => e.stopPropagation()}
177
+ autoFocus
178
+ style={{
179
+ flexGrow: 1,
180
+ minWidth: 0,
181
+ width: 0,
182
+ fontSize: 13,
183
+ padding: "0 2px",
184
+ border: "1px solid #2680eb",
185
+ outline: "none",
186
+ background: "transparent",
187
+ color: "inherit",
188
+ }}
189
+ />
190
+ ) : (
191
+ <span
192
+ style={{
193
+ fontSize: 13,
194
+ flexGrow: 1,
195
+ minWidth: 0,
196
+ overflow: "hidden",
197
+ textOverflow: "ellipsis",
198
+ whiteSpace: "nowrap",
199
+ }}
200
+ onDoubleClick={(e) => {
201
+ e.stopPropagation();
202
+ setEditValue(displayName);
203
+ setIsEditing(true);
204
+ }}
205
+ >
206
+ {displayName}
207
+ </span>
208
+ )}
209
+
210
+ {isMouseOver && !isEditing && parentId && childIndex >= 0 && (
211
+ <span className="layer-move-buttons" style={{ display: "inline-flex", gap: 2, marginLeft: 4, flexShrink: 0 }}>
212
+ {childIndex > 0 && (
213
+ <span
214
+ title="Move up"
215
+ style={{ cursor: "pointer", padding: "0 2px" }}
216
+ onClick={(e) => {
217
+ e.stopPropagation();
218
+ editorActions.move(id, parentId, childIndex - 1);
219
+ }}
220
+ onMouseDown={(e) => e.stopPropagation()}
221
+ >
222
+ <FontAwesomeIcon icon={faArrowUp} fontSize={10} />
223
+ </span>
224
+ )}
225
+ {childIndex >= 0 && childIndex < siblingCount - 1 && (
226
+ <span
227
+ title="Move down"
228
+ style={{ cursor: "pointer", padding: "0 2px" }}
229
+ onClick={(e) => {
230
+ e.stopPropagation();
231
+ editorActions.move(id, parentId, childIndex + 2);
232
+ }}
233
+ onMouseDown={(e) => e.stopPropagation()}
234
+ >
235
+ <FontAwesomeIcon icon={faArrowDown} fontSize={10} />
236
+ </span>
237
+ )}
238
+ {canMoveOut && (
239
+ <span
240
+ title="Move out of container"
241
+ style={{ cursor: "pointer", padding: "0 2px" }}
242
+ onClick={(e) => {
243
+ e.stopPropagation();
244
+ try {
245
+ const parentData = query.node(parentId).get();
246
+ const grandparentId = parentData.data.parent;
247
+ const grandparentData = query.node(grandparentId).get();
248
+ const greatGrandparentId = grandparentData.data.parent;
249
+ const greatGrandparentChildren = query.node(greatGrandparentId).childNodes();
250
+ const grandparentIndex = greatGrandparentChildren.indexOf(grandparentId);
251
+ editorActions.move(id, greatGrandparentId, grandparentIndex >= 0 ? grandparentIndex + 1 : greatGrandparentChildren.length);
252
+ } catch (err) {
253
+ console.warn("Move out failed:", err);
254
+ }
255
+ }}
256
+ onMouseDown={(e) => e.stopPropagation()}
257
+ >
258
+ <FontAwesomeIcon icon={faLevelUpAlt} fontSize={10} />
259
+ </span>
260
+ )}
261
+ </span>
262
+ )}
263
+
264
+ {hasNodes && (
265
+ <span
266
+ onClick={(e) => {
267
+ e.stopPropagation();
268
+ toggleLayer();
269
+ }}
270
+ >
271
+ <FontAwesomeIcon
272
+ icon={expanded ? faChevronUp : faChevronDown}
273
+ fontSize={14}
274
+ className="float-end fa-lg"
275
+ />
276
+ </span>
277
+ )}
278
+ </div>
279
+
280
+ {children}
281
+ </div>
282
+ );
283
+ };
284
+
285
+ export default CustomLayer;
@@ -4,7 +4,9 @@
4
4
  * @subcategory components / elements
5
5
  */
6
6
 
7
- import React, { Fragment, useState, useContext, useEffect } from "react";
7
+ /* globals validate_bool_expression_elem */
8
+
9
+ import React, { Fragment, useState, useContext, useEffect, useRef } from "react";
8
10
  import { useNode } from "@craftjs/core";
9
11
  import useTranslation from "../../hooks/useTranslation";
10
12
  import optionsCtx from "../context";
@@ -73,6 +75,7 @@ const DropDownFilterSettings = () => {
73
75
  }));
74
76
  const options = useContext(optionsCtx);
75
77
  const setAProp = setAPropGen(setProp);
78
+ const editorRef = useRef(null);
76
79
  let select_all_options;
77
80
  const field = options.fields.find((f) => f.name === name);
78
81
  if (field?.type === "String" && field.attributes?.options)
@@ -121,9 +124,13 @@ const DropDownFilterSettings = () => {
121
124
  </td>
122
125
  <td>
123
126
  <SingleLineEditor
127
+ ref={editorRef}
124
128
  value={where}
125
129
  setProp={setProp}
126
130
  propKey="where"
131
+ onInput={(value) =>
132
+ validate_bool_expression_elem(value, editorRef.current)
133
+ }
127
134
  />
128
135
  </td>
129
136
  </tr>
@@ -33,7 +33,7 @@ export /**
33
33
  * @subcategory components
34
34
  */
35
35
  const DropMenu = ({
36
- children,
36
+ contents,
37
37
  action_style,
38
38
  action_size,
39
39
  action_icon,
@@ -50,7 +50,6 @@ const DropMenu = ({
50
50
  connectors: { connect, drag },
51
51
  } = useNode((node) => ({ selected: node.events.selected }));
52
52
  const [showDropdown, setDropdown] = useState(false);
53
- //const [dropWidth, setDropWidth] = useState(200);
54
53
  return (
55
54
  <div
56
55
  className={`${selected ? "selected-node" : ""} ${block ? "d-block" : "d-inline"}`}
@@ -82,7 +81,9 @@ const DropMenu = ({
82
81
  showDropdown ? "show" : ""
83
82
  } ${menu_direction === "end" ? "dropdown-menu-end" : ""}`}
84
83
  >
85
- <div className="canvas d-flex flex-column">{children}</div>
84
+ <Element canvas id="dropmenu-contents" is={Column}>
85
+ {contents}
86
+ </Element>
86
87
  </div>
87
88
  </div>
88
89
  );
@@ -177,8 +178,13 @@ DropMenu.craft = {
177
178
  related: {
178
179
  settings: DropMenuSettings,
179
180
  segment_type: "dropdown_menu",
180
- hasContents: true,
181
181
  fields: [
182
+ {
183
+ label: "Contents",
184
+ name: "contents",
185
+ type: "Nodes",
186
+ nodeID: "dropmenu-contents",
187
+ },
182
188
  "label",
183
189
  "block",
184
190
  "action_style",
@@ -45,7 +45,9 @@ const fields = (mode) => {
45
45
  {
46
46
  label: "HTML Code",
47
47
  name: "text",
48
- type: "textarea",
48
+ input_type: "code",
49
+ type: "code",
50
+ attributes: { mode: "text/html" },
49
51
  segment_name: "contents",
50
52
  onSave: (segment, node_props) => {
51
53
  const div = document.createElement("div");
@@ -3,6 +3,24 @@ import optionsCtx from "../context";
3
3
 
4
4
  import Editor, { useMonaco } from "@monaco-editor/react";
5
5
 
6
+ export const mimeToMonacoLanguage = (mode) => {
7
+ if (!mode) return "typescript";
8
+ const map = {
9
+ "text/javascript": "typescript",
10
+ "application/javascript": "typescript",
11
+ "text/html": "html",
12
+ "text/css": "css",
13
+ "application/json": "json",
14
+ "text/x-sql": "sql",
15
+ "text/x-python": "python",
16
+ "text/x-yaml": "yaml",
17
+ "text/xml": "xml",
18
+ "text/x-markdown": "markdown",
19
+ "text/typescript": "typescript",
20
+ };
21
+ return map[mode] || mode;
22
+ };
23
+
6
24
  const setMonacoLanguage = async (monaco, options, isStatements) => {
7
25
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
8
26
  noLib: true,
@@ -27,33 +45,108 @@ const setMonacoLanguage = async (monaco, options, isStatements) => {
27
45
  });
28
46
  };
29
47
 
48
+ // add hidden-prefix
49
+ // line 1 holds the typed prefix, the user edits from line 2 onwards
50
+ const setupVirtualPrefix = (editor, monaco) => {
51
+ const model = editor.getModel();
52
+ editor.setHiddenAreas([{ startLineNumber: 1, endLineNumber: 1 }]);
53
+ editor.onDidChangeCursorPosition((e) => {
54
+ if (e.position.lineNumber < 2)
55
+ editor.setPosition({ lineNumber: 2, column: 1 });
56
+ });
57
+ editor.onKeyDown((e) => {
58
+ const pos = editor.getPosition();
59
+ if (
60
+ pos.lineNumber === 2 &&
61
+ pos.column === 1 &&
62
+ e.keyCode === monaco.KeyCode.Backspace
63
+ ) {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ }
67
+ });
68
+ // copy without the prefix line
69
+ editor.addAction({
70
+ id: "copy-editable-only",
71
+ label: "Copy Only User Content",
72
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC],
73
+ run: (ed) => {
74
+ const selection = ed.getSelection();
75
+ if (selection.isEmpty()) return;
76
+ const safeSelection = selection.intersectRanges(
77
+ new monaco.Range(
78
+ 2,
79
+ 1,
80
+ model.getLineCount(),
81
+ model.getLineMaxColumn(model.getLineCount())
82
+ )
83
+ );
84
+ if (safeSelection)
85
+ navigator.clipboard.writeText(model.getValueInRange(safeSelection));
86
+ },
87
+ });
88
+ // select all without the prefix line
89
+ editor.addAction({
90
+ id: "select-editable-only",
91
+ label: "Select Only User Content",
92
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyA],
93
+ run: (ed) => {
94
+ ed.setSelection(
95
+ new monaco.Range(
96
+ 2,
97
+ 1,
98
+ model.getLineCount(),
99
+ model.getLineMaxColumn(model.getLineCount())
100
+ )
101
+ );
102
+ },
103
+ });
104
+ };
105
+
30
106
  export const SingleLineEditor = React.forwardRef(
31
- ({ setProp, value, propKey, onChange, onInput, className }, ref) => {
107
+ (
108
+ { setProp, value, propKey, onChange, onInput, className, stateExpr },
109
+ ref
110
+ ) => {
32
111
  const options = React.useContext(optionsCtx);
112
+ const activePrefix = stateExpr ? "const _: Row =" : null;
33
113
 
34
114
  const handleEditorWillMount = (monaco) => {
35
115
  setMonacoLanguage(monaco, options, false);
36
116
  };
37
117
 
38
118
  const handleEditorDidMount = (editor, monaco) => {
119
+ if (activePrefix) {
120
+ setupVirtualPrefix(editor, monaco);
121
+ return;
122
+ }
39
123
  if (!onInput) return;
40
124
 
41
125
  editor.onDidChangeModelContent(() => {
42
- const value = editor.getValue();
43
- onInput(value);
126
+ onInput(editor.getValue());
44
127
  });
45
128
  };
46
129
 
130
+ const isEmpty = !value || value.trim() === "";
131
+ const editorValue = activePrefix
132
+ ? `${isEmpty ? "//" : ""} ${activePrefix}\n${value || ""}`
133
+ : value;
134
+
47
135
  return (
48
136
  <div ref={ref} className="form-control p-0 pt-1">
49
137
  <Editor
50
138
  placeholder={"sdfffsd"}
51
139
  className={className || ""}
52
140
  height="22px"
53
- value={value}
54
- onChange={(value) => {
55
- onChange && onChange(value);
56
- setProp && propKey && setProp((prop) => (prop[propKey] = value));
141
+ value={editorValue}
142
+ onChange={(fullValue) => {
143
+ const userValue = activePrefix
144
+ ? (fullValue || "").substring((fullValue || "").indexOf("\n") + 1)
145
+ : fullValue;
146
+ onChange && onChange(userValue);
147
+ setProp &&
148
+ propKey &&
149
+ setProp((prop) => (prop[propKey] = userValue));
57
150
  }}
58
151
  defaultLanguage="typescript"
59
152
  //onMount={handleEditorDidMount}
@@ -68,25 +161,37 @@ export const SingleLineEditor = React.forwardRef(
68
161
  }
69
162
  );
70
163
 
71
- export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false }) => {
164
+ export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false, mode }) => {
72
165
  const options = React.useContext(optionsCtx);
166
+ const resolvedLanguage = mimeToMonacoLanguage(mode);
167
+ const useTypeScriptSetup = resolvedLanguage === "typescript" || resolvedLanguage === "javascript";
73
168
 
74
169
  const handleEditorWillMount = (monaco) => {
75
- setMonacoLanguage(monaco, options, true);
170
+ if (useTypeScriptSetup) {
171
+ setMonacoLanguage(monaco, options, true);
172
+ }
76
173
  };
174
+
175
+ const handleEditorDidMount = (editor, monaco) => {
176
+ if (!useTypeScriptSetup) {
177
+ const model = editor.getModel();
178
+ if (model) {
179
+ monaco.editor.setModelLanguage(model, resolvedLanguage);
180
+ }
181
+ }
182
+ };
183
+
77
184
  return (
78
185
  <div className="form-control p-0 pt-2">
79
186
  <Editor
80
187
  height={isModalEditor ? "100%" : "150px"}
81
188
  value={value}
82
189
  onChange={onChange}
83
- defaultLanguage="typescript"
84
- //onMount={handleEditorDidMount}
85
- //beforeMount={handleEditorWillMount}
190
+ defaultLanguage={resolvedLanguage}
86
191
  options={multiLineEditorOptions}
87
- //theme="myCoolTheme"
88
192
  beforeMount={handleEditorWillMount}
89
- className={isModalEditor ? 'code-modal-form' : ''}
193
+ onMount={handleEditorDidMount}
194
+ className={isModalEditor ? "code-modal-form" : ""}
90
195
  />
91
196
  </div>
92
197
  );
@@ -94,7 +199,7 @@ export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor =
94
199
 
95
200
  const multiLineEditorOptions = {
96
201
  fontSize: "14px",
97
- minHeight:"80vh",
202
+ minHeight: "80vh",
98
203
  fontWeight: "normal",
99
204
  wordWrap: "off",
100
205
  lineNumbers: "off",