@saltcorn/builder 1.7.0-alpha.1 → 1.7.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.
- package/dist/builder_bundle.js +86 -4207
- package/package.json +3 -2
- package/src/components/Builder.js +41 -5
- package/src/components/Library.js +272 -78
- package/src/components/RenderNode.js +53 -13
- package/src/components/Toolbox.js +74 -0
- package/src/components/elements/LibraryInstance.js +83 -0
- package/src/components/elements/LibrarySlotInstance.js +214 -0
- package/src/components/elements/MessageForm.js +101 -0
- package/src/components/elements/MessageList.js +167 -0
- package/src/components/elements/MonacoEditor.js +39 -27
- package/src/components/storage.js +192 -5
|
@@ -57,6 +57,7 @@ const RenderNode = ({ render }) => {
|
|
|
57
57
|
}));
|
|
58
58
|
|
|
59
59
|
const currentRef = useRef();
|
|
60
|
+
const borderRef = useRef();
|
|
60
61
|
|
|
61
62
|
const getZoomFactor = useCallback(() => {
|
|
62
63
|
const bw = document.body.offsetWidth;
|
|
@@ -65,11 +66,30 @@ const RenderNode = ({ render }) => {
|
|
|
65
66
|
return factor || 1;
|
|
66
67
|
}, []);
|
|
67
68
|
|
|
69
|
+
// a display:contents element (e.g. the LibraryInstance boundary) has no
|
|
70
|
+
// box of its own, so getBoundingClientRect() on it is all zeros - fall
|
|
71
|
+
// back to the union of its children's boxes so selection still has
|
|
72
|
+
// something real to draw a border around
|
|
73
|
+
const getRect = (dom) => {
|
|
74
|
+
if (!dom) return { top: 0, left: 0, bottom: 0, right: 0, height: 0, width: 0 };
|
|
75
|
+
const rect = dom.getBoundingClientRect();
|
|
76
|
+
if (rect.width || rect.height) return rect;
|
|
77
|
+
const childRects = Array.from(dom.children || []).map(getRect);
|
|
78
|
+
if (!childRects.length) return rect;
|
|
79
|
+
const top = Math.min(...childRects.map((r) => r.top));
|
|
80
|
+
const left = Math.min(...childRects.map((r) => r.left));
|
|
81
|
+
const bottom = Math.max(...childRects.map((r) => r.bottom));
|
|
82
|
+
const right = Math.max(...childRects.map((r) => r.right));
|
|
83
|
+
return { top, left, bottom, right, width: right - left, height: bottom - top };
|
|
84
|
+
};
|
|
85
|
+
|
|
68
86
|
const getPos = useCallback((dom) => {
|
|
69
87
|
const zoom = getZoomFactor();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
const ownRect = dom?.getBoundingClientRect();
|
|
89
|
+
const hasOwnBox = !!(ownRect && (ownRect.width || ownRect.height));
|
|
90
|
+
const { top, left, bottom, height, width, right } = hasOwnBox
|
|
91
|
+
? ownRect
|
|
92
|
+
: getRect(dom);
|
|
73
93
|
const topAdj = (top > 0 ? top : bottom) / zoom;
|
|
74
94
|
const leftAdj = left / zoom;
|
|
75
95
|
const rightPos = window.innerWidth / zoom - right / zoom;
|
|
@@ -83,21 +103,23 @@ const RenderNode = ({ render }) => {
|
|
|
83
103
|
height: height / zoom,
|
|
84
104
|
width: width / zoom,
|
|
85
105
|
bottom: bottom / zoom,
|
|
106
|
+
hasOwnBox,
|
|
86
107
|
};
|
|
87
108
|
}, [getZoomFactor]);
|
|
88
109
|
|
|
89
110
|
const scroll = useCallback(() => {
|
|
90
|
-
const { current: currentDOM } = currentRef;
|
|
91
|
-
if (!currentDOM) return;
|
|
92
111
|
const pos = getPos(dom);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
[currentRef.current, borderRef.current].forEach((el) => {
|
|
113
|
+
if (!el) return;
|
|
114
|
+
el.style.top = pos.top;
|
|
115
|
+
if (options.isRTL) {
|
|
116
|
+
el.style.right = pos.right;
|
|
117
|
+
el.style.left = "auto";
|
|
118
|
+
} else {
|
|
119
|
+
el.style.left = pos.left;
|
|
120
|
+
el.style.right = "auto";
|
|
121
|
+
}
|
|
122
|
+
});
|
|
101
123
|
}, [dom, getPos, options.isRTL]);
|
|
102
124
|
|
|
103
125
|
const hiddenColumnParents = new Set(["Card", "Container", "Table", "DropMenu"]);
|
|
@@ -212,6 +234,24 @@ const RenderNode = ({ render }) => {
|
|
|
212
234
|
document.querySelector("#builder-main-canvas")
|
|
213
235
|
)
|
|
214
236
|
: null}
|
|
237
|
+
{isActive && id !== "ROOT" && !getPos(dom).hasOwnBox
|
|
238
|
+
? ReactDOM.createPortal(
|
|
239
|
+
<div
|
|
240
|
+
ref={borderRef}
|
|
241
|
+
className="selected-indicator-border"
|
|
242
|
+
style={{
|
|
243
|
+
...(options.isRTL
|
|
244
|
+
? { right: getPos(dom).right, left: "auto" }
|
|
245
|
+
: { left: getPos(dom).left, right: "auto" }),
|
|
246
|
+
top: getPos(dom).top,
|
|
247
|
+
width: getPos(dom).width,
|
|
248
|
+
height: getPos(dom).height,
|
|
249
|
+
zIndex: 1028,
|
|
250
|
+
}}
|
|
251
|
+
/>,
|
|
252
|
+
document.querySelector("#builder-main-canvas")
|
|
253
|
+
)
|
|
254
|
+
: null}
|
|
215
255
|
{render}
|
|
216
256
|
</>
|
|
217
257
|
);
|
|
@@ -27,6 +27,8 @@ import { Container } from "./elements/Container";
|
|
|
27
27
|
import { Image } from "./elements/Image";
|
|
28
28
|
import { View } from "./elements/View";
|
|
29
29
|
import { SearchBar } from "./elements/SearchBar";
|
|
30
|
+
import { MessageList } from "./elements/MessageList";
|
|
31
|
+
import { MessageForm } from "./elements/MessageForm";
|
|
30
32
|
import { Link } from "./elements/Link";
|
|
31
33
|
import { Page } from "./elements/Page";
|
|
32
34
|
import { Prompt } from "./elements/Prompt";
|
|
@@ -866,6 +868,48 @@ const ToolboxEdit = ({ expanded }) => {
|
|
|
866
868
|
);
|
|
867
869
|
};
|
|
868
870
|
|
|
871
|
+
/**
|
|
872
|
+
* @param {object} props
|
|
873
|
+
* @param {object} props.connectors
|
|
874
|
+
* @returns {WrapElem}
|
|
875
|
+
* @category saltcorn-builder
|
|
876
|
+
* @subcategory components / Toolbox
|
|
877
|
+
* @namespace
|
|
878
|
+
*/
|
|
879
|
+
const MessageListElem = ({ connectors }) => {
|
|
880
|
+
const { t } = useTranslation();
|
|
881
|
+
return (
|
|
882
|
+
<WrapElem
|
|
883
|
+
connectors={connectors}
|
|
884
|
+
icon="fas fa-comments"
|
|
885
|
+
title={t("The list of messages in the room")}
|
|
886
|
+
label="Messages"
|
|
887
|
+
>
|
|
888
|
+
<Element canvas is={MessageList}></Element>
|
|
889
|
+
</WrapElem>
|
|
890
|
+
);
|
|
891
|
+
};
|
|
892
|
+
/**
|
|
893
|
+
* @param {object} props
|
|
894
|
+
* @param {object} props.connectors
|
|
895
|
+
* @returns {WrapElem}
|
|
896
|
+
* @category saltcorn-builder
|
|
897
|
+
* @subcategory components / Toolbox
|
|
898
|
+
* @namespace
|
|
899
|
+
*/
|
|
900
|
+
const MessageFormElem = ({ connectors }) => {
|
|
901
|
+
const { t } = useTranslation();
|
|
902
|
+
return (
|
|
903
|
+
<WrapElem
|
|
904
|
+
connectors={connectors}
|
|
905
|
+
icon="fas fa-paper-plane"
|
|
906
|
+
title={t("The form for sending a new message")}
|
|
907
|
+
label="Send"
|
|
908
|
+
>
|
|
909
|
+
<Element canvas is={MessageForm}></Element>
|
|
910
|
+
</WrapElem>
|
|
911
|
+
);
|
|
912
|
+
};
|
|
869
913
|
export /**
|
|
870
914
|
* @returns {Fragment}
|
|
871
915
|
* @category saltcorn-builder
|
|
@@ -901,3 +945,33 @@ const ToolboxPage = ({ expanded }) => {
|
|
|
901
945
|
expanded
|
|
902
946
|
);
|
|
903
947
|
};
|
|
948
|
+
|
|
949
|
+
export /**
|
|
950
|
+
* @returns {Fragment}
|
|
951
|
+
* @category saltcorn-builder
|
|
952
|
+
* @subcategory components / Toolbox
|
|
953
|
+
* @namespace
|
|
954
|
+
*/
|
|
955
|
+
const ToolboxRoom = ({ expanded }) => {
|
|
956
|
+
const { connectors } = useEditor();
|
|
957
|
+
const options = useContext(optionsCtx);
|
|
958
|
+
const { views, images, pages } = options;
|
|
959
|
+
return chunkToolBox(
|
|
960
|
+
[
|
|
961
|
+
<MessageListElem connectors={connectors} />,
|
|
962
|
+
<MessageFormElem connectors={connectors} />,
|
|
963
|
+
<TextElem connectors={connectors} />,
|
|
964
|
+
<ColumnsElem connectors={connectors} />,
|
|
965
|
+
<LineBreakElem connectors={connectors} />,
|
|
966
|
+
<ContainerElem connectors={connectors} />,
|
|
967
|
+
<CardElem connectors={connectors} />,
|
|
968
|
+
<TabsElem connectors={connectors} />,
|
|
969
|
+
<LinkElem connectors={connectors} />,
|
|
970
|
+
<ViewElem connectors={connectors} views={views} />,
|
|
971
|
+
<ImageElem connectors={connectors} images={images} />,
|
|
972
|
+
<HTMLElem connectors={connectors} />,
|
|
973
|
+
<PageElem connectors={connectors} pages={pages} />,
|
|
974
|
+
],
|
|
975
|
+
expanded
|
|
976
|
+
);
|
|
977
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category saltcorn-builder
|
|
3
|
+
* @module components/elements/LibraryInstance
|
|
4
|
+
* @subcategory components / elements
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { useNode } from "@craftjs/core";
|
|
9
|
+
import useTranslation from "../../hooks/useTranslation";
|
|
10
|
+
|
|
11
|
+
// A placed copy of a shared Library component - edits here save back to the
|
|
12
|
+
// shared component, so every other page using it updates too.
|
|
13
|
+
export /**
|
|
14
|
+
* @param {object} props
|
|
15
|
+
* @param {*} props.children
|
|
16
|
+
* @returns {div}
|
|
17
|
+
* @category saltcorn-builder
|
|
18
|
+
* @subcategory components
|
|
19
|
+
* @namespace
|
|
20
|
+
*/
|
|
21
|
+
const LibraryInstance = ({ children }) => {
|
|
22
|
+
const {
|
|
23
|
+
selected,
|
|
24
|
+
connectors: { connect, drag },
|
|
25
|
+
} = useNode((node) => ({ selected: node.events.selected }));
|
|
26
|
+
|
|
27
|
+
// a real box (not display:contents) so drops can actually be targeted
|
|
28
|
+
// inside it - display:contents has no geometry, so the builder's own
|
|
29
|
+
// drop-position hit-testing skips straight past it to its parent
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
ref={(dom) => connect(drag(dom))}
|
|
33
|
+
className={`library-instance ${selected ? "selected-node" : ""}`}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export /**
|
|
41
|
+
* @returns {div}
|
|
42
|
+
* @category saltcorn-builder
|
|
43
|
+
* @subcategory components
|
|
44
|
+
* @namespace
|
|
45
|
+
*/
|
|
46
|
+
const LibraryInstanceSettings = () => {
|
|
47
|
+
const { library_name } = useNode((node) => ({
|
|
48
|
+
library_name: node.data.props.library_name,
|
|
49
|
+
}));
|
|
50
|
+
const { t } = useTranslation();
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
<p>
|
|
54
|
+
{t("Shared component")}: <strong>{library_name}</strong>
|
|
55
|
+
</p>
|
|
56
|
+
<small className="text-muted d-block">
|
|
57
|
+
{t(
|
|
58
|
+
"Editing its contents changes every page and view that uses it."
|
|
59
|
+
)}
|
|
60
|
+
</small>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @type {object}
|
|
67
|
+
*/
|
|
68
|
+
LibraryInstance.craft = {
|
|
69
|
+
displayName: "LibraryInstance",
|
|
70
|
+
// declared here rather than relying on every caller to wrap it in
|
|
71
|
+
// <Element canvas> - some (eg Library.js's addSelected) create it bare
|
|
72
|
+
isCanvas: true,
|
|
73
|
+
props: {
|
|
74
|
+
library_id: undefined,
|
|
75
|
+
library_name: "",
|
|
76
|
+
},
|
|
77
|
+
rules: {
|
|
78
|
+
canDrag: () => true,
|
|
79
|
+
},
|
|
80
|
+
related: {
|
|
81
|
+
settings: LibraryInstanceSettings,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category saltcorn-builder
|
|
3
|
+
* @module components/elements/LibrarySlotInstance
|
|
4
|
+
* @subcategory components / elements
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useContext } from "react";
|
|
8
|
+
import { useNode } from "@craftjs/core";
|
|
9
|
+
import optionsCtx from "../context";
|
|
10
|
+
import useTranslation from "../../hooks/useTranslation";
|
|
11
|
+
|
|
12
|
+
export /**
|
|
13
|
+
* A placeholder inside a shared Library component - each placement fills it
|
|
14
|
+
* independently (picks a field to render, or drops its own content in),
|
|
15
|
+
* without touching the shared component itself.
|
|
16
|
+
* @param {object} props
|
|
17
|
+
* @param {string} props.kind
|
|
18
|
+
* @param {string} props.slot_name
|
|
19
|
+
* @param {string} props.field
|
|
20
|
+
* @param {string} props.fieldview
|
|
21
|
+
* @param {*} props.children
|
|
22
|
+
* @returns {div}
|
|
23
|
+
* @category saltcorn-builder
|
|
24
|
+
* @subcategory components
|
|
25
|
+
* @namespace
|
|
26
|
+
*/
|
|
27
|
+
const LibrarySlotInstance = ({ kind, slot_name, field, fieldview, children }) => {
|
|
28
|
+
const {
|
|
29
|
+
selected,
|
|
30
|
+
connectors: { connect, drag },
|
|
31
|
+
} = useNode((node) => ({ selected: node.events.selected }));
|
|
32
|
+
const { t } = useTranslation();
|
|
33
|
+
const isEmpty = Array.isArray(children) ? children.length === 0 : !children;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
ref={(dom) => connect(drag(dom))}
|
|
38
|
+
className={`library-slot-instance ${
|
|
39
|
+
kind === "field" ? "d-inline-block" : ""
|
|
40
|
+
} ${selected ? "selected-node" : ""}`}
|
|
41
|
+
>
|
|
42
|
+
{kind === "field" ? (
|
|
43
|
+
field ? (
|
|
44
|
+
`[slot: ${fieldview || ""} ${field}]`
|
|
45
|
+
) : (
|
|
46
|
+
`[slot: ${slot_name || "unfilled"}]`
|
|
47
|
+
)
|
|
48
|
+
) : isEmpty ? (
|
|
49
|
+
// an empty container slot has no size of its own to drop onto - a
|
|
50
|
+
// placeholder gives it one, for both real users and drag targeting
|
|
51
|
+
<div
|
|
52
|
+
className="text-muted border border-dashed p-2"
|
|
53
|
+
style={{ minHeight: "2em" }}
|
|
54
|
+
>
|
|
55
|
+
{t("Drop content here")}
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
58
|
+
children
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export /**
|
|
65
|
+
* @returns {div}
|
|
66
|
+
* @category saltcorn-builder
|
|
67
|
+
* @subcategory components
|
|
68
|
+
* @namespace
|
|
69
|
+
*/
|
|
70
|
+
const LibrarySlotInstanceSettings = () => {
|
|
71
|
+
const {
|
|
72
|
+
actions: { setProp },
|
|
73
|
+
slot_name,
|
|
74
|
+
kind,
|
|
75
|
+
field,
|
|
76
|
+
fieldview,
|
|
77
|
+
} = useNode((node) => ({
|
|
78
|
+
slot_name: node.data.props.slot_name,
|
|
79
|
+
kind: node.data.props.kind,
|
|
80
|
+
field: node.data.props.field,
|
|
81
|
+
fieldview: node.data.props.fieldview,
|
|
82
|
+
}));
|
|
83
|
+
const { t } = useTranslation();
|
|
84
|
+
const options = useContext(optionsCtx);
|
|
85
|
+
const canPickField = !!options.fields;
|
|
86
|
+
const fvs = (options.field_view_options || {})[field];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<label>{t("Slot name")}</label>
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
className="slot-name form-control"
|
|
94
|
+
value={slot_name || ""}
|
|
95
|
+
onChange={(e) => {
|
|
96
|
+
if (!e.target) return;
|
|
97
|
+
const value = e.target.value;
|
|
98
|
+
setProp((prop) => (prop.slot_name = value));
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
<label className="mt-2">{t("Kind")}</label>
|
|
102
|
+
<select
|
|
103
|
+
className="slot-kind form-control form-select"
|
|
104
|
+
value={kind}
|
|
105
|
+
onChange={(e) => {
|
|
106
|
+
if (!e.target) return;
|
|
107
|
+
const value = e.target.value;
|
|
108
|
+
setProp((prop) => (prop.kind = value));
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<option value="field" disabled={!canPickField}>
|
|
112
|
+
{t("Field")}
|
|
113
|
+
</option>
|
|
114
|
+
<option value="container">{t("Container")}</option>
|
|
115
|
+
</select>
|
|
116
|
+
{!canPickField && kind === "field" && (
|
|
117
|
+
<small className="text-muted d-block mt-1">
|
|
118
|
+
{t(
|
|
119
|
+
"Field slots aren't available here - this builder isn't bound to one table."
|
|
120
|
+
)}
|
|
121
|
+
</small>
|
|
122
|
+
)}
|
|
123
|
+
{kind === "field" && canPickField ? (
|
|
124
|
+
<table className="w-100 mt-2">
|
|
125
|
+
<tbody>
|
|
126
|
+
<tr>
|
|
127
|
+
<td>
|
|
128
|
+
<label>{t("Field")}</label>
|
|
129
|
+
</td>
|
|
130
|
+
<td>
|
|
131
|
+
<select
|
|
132
|
+
className="field form-control form-select"
|
|
133
|
+
value={field || ""}
|
|
134
|
+
onChange={(e) => {
|
|
135
|
+
if (!e.target) return;
|
|
136
|
+
const value = e.target.value;
|
|
137
|
+
setProp((prop) => (prop.field = value));
|
|
138
|
+
const newfvs = options.field_view_options[value];
|
|
139
|
+
if (newfvs && newfvs.length > 0)
|
|
140
|
+
setProp((prop) => (prop.fieldview = newfvs[0]));
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<option value="">{t("Choose a field")}</option>
|
|
144
|
+
{options.fields.map((f, ix) => (
|
|
145
|
+
<option key={ix} value={f.name}>
|
|
146
|
+
{f.label}
|
|
147
|
+
</option>
|
|
148
|
+
))}
|
|
149
|
+
</select>
|
|
150
|
+
</td>
|
|
151
|
+
</tr>
|
|
152
|
+
{fvs && (
|
|
153
|
+
<tr>
|
|
154
|
+
<td>
|
|
155
|
+
<label>{t("Field view")}</label>
|
|
156
|
+
</td>
|
|
157
|
+
<td>
|
|
158
|
+
<select
|
|
159
|
+
className="fieldview form-control form-select"
|
|
160
|
+
value={fieldview || ""}
|
|
161
|
+
onChange={(e) => {
|
|
162
|
+
if (!e.target) return;
|
|
163
|
+
const value = e.target.value;
|
|
164
|
+
setProp((prop) => (prop.fieldview = value));
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{(fvs || []).map((fvnm, ix) => (
|
|
168
|
+
<option key={ix} value={fvnm}>
|
|
169
|
+
{fvnm}
|
|
170
|
+
</option>
|
|
171
|
+
))}
|
|
172
|
+
</select>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
)}
|
|
176
|
+
</tbody>
|
|
177
|
+
</table>
|
|
178
|
+
) : null}
|
|
179
|
+
{kind === "container" && (
|
|
180
|
+
<small className="text-muted d-block mt-2">
|
|
181
|
+
{t(
|
|
182
|
+
"Drop elements here - each placement of this component fills this area independently."
|
|
183
|
+
)}
|
|
184
|
+
</small>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @type {object}
|
|
192
|
+
*/
|
|
193
|
+
LibrarySlotInstance.craft = {
|
|
194
|
+
displayName: "LibrarySlotInstance",
|
|
195
|
+
// declared here rather than relying on every caller to wrap it in
|
|
196
|
+
// <Element canvas> - Library.js's onNodesChange creates it bare
|
|
197
|
+
isCanvas: true,
|
|
198
|
+
props: {
|
|
199
|
+
slot_name: "",
|
|
200
|
+
kind: "field",
|
|
201
|
+
field: undefined,
|
|
202
|
+
fieldview: undefined,
|
|
203
|
+
},
|
|
204
|
+
rules: {
|
|
205
|
+
canDrag: () => true,
|
|
206
|
+
// a field slot has no children - it's filled via the settings panel,
|
|
207
|
+
// not by dropping elements into it
|
|
208
|
+
canMoveIn: (incoming, currentNode) =>
|
|
209
|
+
currentNode?.data?.props?.kind !== "field",
|
|
210
|
+
},
|
|
211
|
+
related: {
|
|
212
|
+
settings: LibrarySlotInstanceSettings,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category saltcorn-builder
|
|
3
|
+
* @module components/elements/MessageForm
|
|
4
|
+
* @subcategory components / elements
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import useTranslation from "../../hooks/useTranslation";
|
|
9
|
+
import { useNode } from "@craftjs/core";
|
|
10
|
+
import { setAPropGen } from "./utils";
|
|
11
|
+
|
|
12
|
+
export /**
|
|
13
|
+
* The form for sending a new message. Its fields come from the room's message
|
|
14
|
+
* form view, so this stands in for it on the canvas.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} props
|
|
17
|
+
* @param {boolean} props.pinned
|
|
18
|
+
* @returns {div}
|
|
19
|
+
* @namespace
|
|
20
|
+
* @category saltcorn-builder
|
|
21
|
+
* @subcategory components
|
|
22
|
+
*/
|
|
23
|
+
const MessageForm = ({ pinned }) => {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const {
|
|
26
|
+
selected,
|
|
27
|
+
connectors: { connect, drag },
|
|
28
|
+
} = useNode((node) => ({ selected: node.events.selected }));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={`sc-room-msgform-preview ${selected ? "selected-node" : ""}`}
|
|
33
|
+
ref={(dom) => connect(drag(dom))}
|
|
34
|
+
>
|
|
35
|
+
<div className="input-group">
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
className="form-control bg-light"
|
|
39
|
+
placeholder={t("New message...")}
|
|
40
|
+
readOnly={true}
|
|
41
|
+
/>
|
|
42
|
+
<button className="btn btn-primary" disabled>
|
|
43
|
+
{t("Send")}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
{pinned && (
|
|
47
|
+
<div className="text-muted small">{t("Pinned below the messages")}</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export /**
|
|
54
|
+
* @returns {div}
|
|
55
|
+
* @namespace
|
|
56
|
+
* @category saltcorn-builder
|
|
57
|
+
* @subcategory components
|
|
58
|
+
*/
|
|
59
|
+
const MessageFormSettings = () => {
|
|
60
|
+
const { t } = useTranslation();
|
|
61
|
+
const {
|
|
62
|
+
actions: { setProp },
|
|
63
|
+
pinned,
|
|
64
|
+
} = useNode((node) => ({ pinned: node.data.props.pinned }));
|
|
65
|
+
const setAProp = setAPropGen(setProp);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<div className="form-check">
|
|
70
|
+
<input
|
|
71
|
+
className="form-check-input"
|
|
72
|
+
name="pinned"
|
|
73
|
+
type="checkbox"
|
|
74
|
+
checked={!!pinned}
|
|
75
|
+
onChange={setAProp("pinned", { checked: true })}
|
|
76
|
+
/>
|
|
77
|
+
<label className="form-check-label">{t("Pin to bottom")}</label>
|
|
78
|
+
</div>
|
|
79
|
+
<small className="text-muted">
|
|
80
|
+
{t(
|
|
81
|
+
"Fix the form below the message list, which then scrolls within the room. The room takes the message list height, or fills the window down to the bottom if that is blank"
|
|
82
|
+
)}
|
|
83
|
+
</small>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @type {object}
|
|
90
|
+
*/
|
|
91
|
+
MessageForm.craft = {
|
|
92
|
+
displayName: "MessageForm",
|
|
93
|
+
props: {
|
|
94
|
+
pinned: false,
|
|
95
|
+
},
|
|
96
|
+
related: {
|
|
97
|
+
settings: MessageFormSettings,
|
|
98
|
+
segment_type: "message_form",
|
|
99
|
+
fields: [{ name: "pinned" }],
|
|
100
|
+
},
|
|
101
|
+
};
|