@vonaffenfels/slate-editor 1.0.3 → 1.0.4
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/BlockEditor.css +4 -4
- package/dist/BlockEditor.js +1 -1
- package/dist/BlockEditor.js.LICENSE.txt +0 -33
- package/dist/Renderer.js +1 -1
- package/dist/index.css +4 -4
- package/dist/index.js +1 -1
- package/dist/index.js.LICENSE.txt +0 -33
- package/package.json +2 -2
- package/scss/demo.scss +7 -0
- package/scss/editor.scss +70 -38
- package/scss/sidebarEditor.scss +60 -21
- package/scss/storybook.scss +73 -5
- package/scss/toolbar.scss +2 -0
- package/src/BlockEditor.js +85 -45
- package/src/Nodes/Element.js +11 -9
- package/src/Nodes/Storybook.js +13 -32
- package/src/SidebarEditor/AssetList.js +129 -0
- package/src/SidebarEditor/SidebarEditorField.js +132 -64
- package/src/SidebarEditor.js +270 -121
- package/src/Toolbar/Toolbar.js +39 -34
- package/src/dev/App.js +6 -0
- package/src/dev/testComponents/TestStory.js +68 -2
- package/src/dev/testComponents/TestStory.stories.js +13 -3
- package/src/helper/array.js +9 -0
- package/webpack.config.build.js +2 -0
package/src/BlockEditor.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React, {
|
|
2
|
-
useCallback, useMemo, useState, useRef,
|
|
2
|
+
useCallback, useMemo, useState, useRef,
|
|
3
3
|
} from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Slate, Editable, withReact, ReactEditor,
|
|
6
6
|
} from 'slate-react';
|
|
7
7
|
import {
|
|
8
|
-
createEditor, Element as SlateElement, Node, Text, Transforms,
|
|
8
|
+
createEditor, Element as SlateElement, Node, Point, Text, Transforms,
|
|
9
9
|
} from 'slate';
|
|
10
10
|
import {withHistory} from 'slate-history';
|
|
11
11
|
import {Leaf} from "./Nodes/Leaf";
|
|
@@ -14,7 +14,6 @@ import {SoftBreakPlugin} from "./plugins/SoftBreak";
|
|
|
14
14
|
import classNames from "classnames";
|
|
15
15
|
import {Element} from "./Nodes/Element";
|
|
16
16
|
import "../scss/editor.scss";
|
|
17
|
-
import {StorybookContext} from "./Context/StorybookContext";
|
|
18
17
|
import {ListItemPlugin} from "./plugins/ListItem";
|
|
19
18
|
import ErrorBoundary from "../src/Blocks/ErrorBoundary";
|
|
20
19
|
import SidebarEditor from './SidebarEditor';
|
|
@@ -27,10 +26,9 @@ export default function BlockEditor({
|
|
|
27
26
|
storybookComponentLoader,
|
|
28
27
|
storybookComponentDataLoader,
|
|
29
28
|
storybookStories,
|
|
29
|
+
onSaveClick,
|
|
30
30
|
}) {
|
|
31
|
-
const
|
|
32
|
-
const storybookTarget = useRef(null);
|
|
33
|
-
const [isStorybookActive, setStorybookActive] = useState(false);
|
|
31
|
+
const scrollContainer = useRef(null);
|
|
34
32
|
const [selectedStorybookElement, setSelectedStorybookElement] = useState(null);
|
|
35
33
|
const renderElement = useCallback((props) => {
|
|
36
34
|
return <Element
|
|
@@ -39,11 +37,30 @@ export default function BlockEditor({
|
|
|
39
37
|
storybookComponentLoader={storybookComponentLoader}
|
|
40
38
|
storybookComponentDataLoader={storybookComponentDataLoader}
|
|
41
39
|
elementPropsMap={elementPropsMap}
|
|
42
|
-
onStorybookElementClick={(element, attributes) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
onStorybookElementClick={(element, attributes, e) => {
|
|
41
|
+
clearSelectionOutlines();
|
|
42
|
+
|
|
43
|
+
if (attributes?.ref?.current) {
|
|
44
|
+
let targetDOMElement = attributes.ref.current.querySelectorAll(".storybook-element-component")[0];
|
|
45
|
+
|
|
46
|
+
if (targetDOMElement) {
|
|
47
|
+
targetDOMElement.classList.add("selected-storybook-element");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setSelectedStorybookElement({
|
|
52
|
+
...element,
|
|
53
|
+
editorAttributes: attributes,
|
|
54
|
+
});
|
|
55
|
+
}}
|
|
56
|
+
onElementClick={(element) => {
|
|
57
|
+
console.log({element});
|
|
58
|
+
let hasBlockElement = element.children.find(child => child.block) !== undefined;
|
|
59
|
+
|
|
60
|
+
if (!hasBlockElement) {
|
|
61
|
+
setSelectedStorybookElement(null);
|
|
62
|
+
}
|
|
63
|
+
}}/>;
|
|
47
64
|
}, []);
|
|
48
65
|
const renderLeaf = useCallback(props => <Leaf {...props} elementPropsMap={elementPropsMap}/>, []);
|
|
49
66
|
const emptyValue = [
|
|
@@ -136,12 +153,20 @@ export default function BlockEditor({
|
|
|
136
153
|
return editor;
|
|
137
154
|
}, []);
|
|
138
155
|
|
|
156
|
+
const clearSelectionOutlines = () => {
|
|
157
|
+
let selectedDOMElements = document.querySelectorAll(".selected-storybook-element");
|
|
158
|
+
|
|
159
|
+
if (selectedDOMElements && selectedDOMElements.length) {
|
|
160
|
+
Array.from(selectedDOMElements).forEach(e => {
|
|
161
|
+
e.classList.remove("selected-storybook-element");
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
139
166
|
const handleSidebarEditorChange = newStorybookElement => {
|
|
140
167
|
let node = ReactEditor.toSlateNode(editor, selectedStorybookElement.editorAttributes.ref.current);
|
|
141
168
|
let path = ReactEditor.findPath(editor, node);
|
|
142
169
|
|
|
143
|
-
console.log({newStorybookElement});
|
|
144
|
-
|
|
145
170
|
let newNodeProps = {
|
|
146
171
|
block: newStorybookElement.block,
|
|
147
172
|
isEmpty: false,
|
|
@@ -159,24 +184,41 @@ export default function BlockEditor({
|
|
|
159
184
|
};
|
|
160
185
|
|
|
161
186
|
const handleSidebarDeleteClick = (element) => {
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
contentfulSdk.dialogs
|
|
188
|
+
.openConfirm({
|
|
189
|
+
title: 'Element löschen',
|
|
190
|
+
message: 'Sind Sie sicher, dass Sie dieses Element löschen wollen?',
|
|
191
|
+
intent: 'negative',
|
|
192
|
+
confirmLabel: 'Löschen',
|
|
193
|
+
cancelLabel: 'Abbrechen',
|
|
194
|
+
})
|
|
195
|
+
.then((result) => {
|
|
196
|
+
if (result) {
|
|
197
|
+
let node = ReactEditor.toSlateNode(editor, element.editorAttributes.ref.current);
|
|
198
|
+
let path = ReactEditor.findPath(editor, node);
|
|
164
199
|
|
|
165
|
-
|
|
166
|
-
|
|
200
|
+
Transforms.setNodes(editor, {isDeleted: true}, {at: path});
|
|
201
|
+
Transforms.removeNodes(editor, {at: path});
|
|
167
202
|
|
|
168
|
-
|
|
203
|
+
clearSelectionOutlines();
|
|
204
|
+
|
|
205
|
+
setSelectedStorybookElement(null);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
169
208
|
};
|
|
170
209
|
|
|
171
210
|
const handleSidebarMoveClick = (element, direction) => {
|
|
172
211
|
let node = ReactEditor.toSlateNode(editor, element.editorAttributes.ref.current);
|
|
173
212
|
let path = ReactEditor.findPath(editor, node);
|
|
174
213
|
|
|
175
|
-
|
|
176
|
-
let to = [path[0] - 1];
|
|
214
|
+
let to;
|
|
177
215
|
|
|
178
|
-
if (direction === "down") {
|
|
216
|
+
if (direction === "down" && path[0] < editor.children.length - 1) {
|
|
179
217
|
to = [path[0] + 1];
|
|
218
|
+
} else if (direction === "up" && path[0] > 0) {
|
|
219
|
+
to = [path[0] - 1];
|
|
220
|
+
} else {
|
|
221
|
+
return;
|
|
180
222
|
}
|
|
181
223
|
|
|
182
224
|
try {
|
|
@@ -194,26 +236,24 @@ export default function BlockEditor({
|
|
|
194
236
|
}
|
|
195
237
|
};
|
|
196
238
|
|
|
239
|
+
const handleSidebarClose = () => {
|
|
240
|
+
clearSelectionOutlines();
|
|
241
|
+
|
|
242
|
+
setSelectedStorybookElement(null);
|
|
243
|
+
};
|
|
244
|
+
|
|
197
245
|
return (
|
|
198
|
-
<div className={classNames({
|
|
199
|
-
"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
isActive: isStorybookActive,
|
|
210
|
-
}}>
|
|
211
|
-
<Slate
|
|
212
|
-
editor={editor}
|
|
213
|
-
value={value}
|
|
214
|
-
onChange={onSlateChange}
|
|
215
|
-
>
|
|
216
|
-
<Toolbar hover={false}/>
|
|
246
|
+
<div className={classNames({"block-editor-wrapper h-full": true})}>
|
|
247
|
+
<div className="slate-editor h-full">
|
|
248
|
+
<Slate
|
|
249
|
+
editor={editor}
|
|
250
|
+
value={value}
|
|
251
|
+
onChange={onSlateChange}
|
|
252
|
+
>
|
|
253
|
+
<div>
|
|
254
|
+
<Toolbar hover={false} onSaveClick={onSaveClick}/>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="h-full max-h-full overflow-scroll px-8 py-4" ref={scrollContainer}>
|
|
217
257
|
<ErrorBoundary
|
|
218
258
|
name="editor"
|
|
219
259
|
fallback={(
|
|
@@ -236,17 +276,17 @@ export default function BlockEditor({
|
|
|
236
276
|
}}
|
|
237
277
|
/>
|
|
238
278
|
</ErrorBoundary>
|
|
239
|
-
</
|
|
240
|
-
</
|
|
279
|
+
</div>
|
|
280
|
+
</Slate>
|
|
241
281
|
</div>
|
|
242
282
|
{!!selectedStorybookElement && <SidebarEditor
|
|
243
283
|
sdk={contentfulSdk}
|
|
244
284
|
storybookElement={selectedStorybookElement}
|
|
245
285
|
onChange={handleSidebarEditorChange}
|
|
246
286
|
storybookStories={storybookStories}
|
|
247
|
-
onClose={
|
|
248
|
-
onDelete={
|
|
249
|
-
onMove={
|
|
287
|
+
onClose={handleSidebarClose}
|
|
288
|
+
onDelete={handleSidebarDeleteClick}
|
|
289
|
+
onMove={handleSidebarMoveClick}/>}
|
|
250
290
|
</div>
|
|
251
291
|
);
|
|
252
292
|
}
|
package/src/Nodes/Element.js
CHANGED
|
@@ -36,15 +36,17 @@ export const Element = ({
|
|
|
36
36
|
|
|
37
37
|
switch (props.element.type) {
|
|
38
38
|
case 'storybook':
|
|
39
|
-
return <
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
return <div className="inline" onClick={e => e.stopPropagation()}>
|
|
40
|
+
<StorybookNode
|
|
41
|
+
{...props}
|
|
42
|
+
{...typeProps}
|
|
43
|
+
isInSlot={isInSlot}
|
|
44
|
+
storybookComponentLoader={storybookComponentLoader}
|
|
45
|
+
storybookComponentDataLoader={storybookComponentDataLoader}
|
|
46
|
+
editor={editor}
|
|
47
|
+
onStorybookElementClick={onStorybookElementClick}
|
|
48
|
+
>{children}</StorybookNode>
|
|
49
|
+
</div>;
|
|
48
50
|
case 'layout':
|
|
49
51
|
return <LayoutBlock
|
|
50
52
|
editor={editor}
|
package/src/Nodes/Storybook.js
CHANGED
|
@@ -21,7 +21,6 @@ const StorybookNodeComponent = ({
|
|
|
21
21
|
storybookComponentDataLoader,
|
|
22
22
|
onStorybookElementClick,
|
|
23
23
|
}) => {
|
|
24
|
-
|
|
25
24
|
const onEditClick = () => {
|
|
26
25
|
onStorybookElementClick(element, attributes);
|
|
27
26
|
};
|
|
@@ -114,41 +113,23 @@ const StorybookNodeComponent = ({
|
|
|
114
113
|
return (
|
|
115
114
|
<span
|
|
116
115
|
{...attributes}
|
|
116
|
+
onClick={() => onStorybookElementClick(element, attributes)}
|
|
117
117
|
className={classNames({
|
|
118
|
-
"storybook-element options-wrapper": true,
|
|
118
|
+
"storybook-element options-wrapper cursor-pointer": true,
|
|
119
|
+
"storybook-inline": element.isInline,
|
|
119
120
|
"storybook-element-float-left": element?.attributes?.float === "left",
|
|
120
121
|
"storybook-element-float-right": element?.attributes?.float === "right",
|
|
121
122
|
})}>
|
|
122
|
-
<span className="
|
|
123
|
-
<span className="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</span>
|
|
133
|
-
<span className="options-container-option options-container-option-expand-text" onClick={switchSize}>
|
|
134
|
-
{!element?.attributes?.blockWidth && "Volle Breite"}
|
|
135
|
-
{element?.attributes?.blockWidth === "article" && "Artikel Breite"}
|
|
136
|
-
{element?.attributes?.blockWidth === "site" && "Seiten Breite"}
|
|
137
|
-
{element?.attributes?.blockWidth === "small" && "Klein"}
|
|
138
|
-
</span>
|
|
139
|
-
<ToolMargin margin={element?.attributes?.margin} onChange={onMarginChange} />
|
|
140
|
-
</>)}
|
|
141
|
-
</span>
|
|
142
|
-
<span className="options-container options-container-right">
|
|
143
|
-
<span className="options-container-option options-container-option-delete" title="Delete" onClick={onDeleteClick}><FontAwesomeIcon
|
|
144
|
-
icon={faTimes}
|
|
145
|
-
size="lg"/></span>
|
|
146
|
-
</span>
|
|
147
|
-
<span className={classNames({
|
|
148
|
-
"storybook-element-component": true,
|
|
149
|
-
"storybook-inline": element.isInline,
|
|
150
|
-
})}>
|
|
151
|
-
<StorybookDisplay {...element.attributes} element={element} storybookComponentLoader={storybookComponentLoader} isEditor={true} storybookComponentDataLoader={storybookComponentDataLoader}/>
|
|
123
|
+
<span className={classNames({"storybook-element-component": true})}>
|
|
124
|
+
<span className="storybook-element-component-overlay" title="Klicken, um das Element zu konfigurieren" onClick={onEditClick}>Klicken, um das Element zu konfigurieren</span>
|
|
125
|
+
<div onClick={onEditClick}>
|
|
126
|
+
<StorybookDisplay
|
|
127
|
+
{...element.attributes}
|
|
128
|
+
element={element}
|
|
129
|
+
storybookComponentLoader={storybookComponentLoader}
|
|
130
|
+
isEditor={true}
|
|
131
|
+
storybookComponentDataLoader={storybookComponentDataLoader}/>
|
|
132
|
+
</div>
|
|
152
133
|
</span>
|
|
153
134
|
{children}
|
|
154
135
|
</span>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect, useState,
|
|
3
|
+
} from "react";
|
|
4
|
+
import {IconButton} from "../SidebarEditor";
|
|
5
|
+
import {swapArrayElements} from "../helper/array";
|
|
6
|
+
|
|
7
|
+
export const AssetList = ({
|
|
8
|
+
assets,
|
|
9
|
+
onChange,
|
|
10
|
+
sdk,
|
|
11
|
+
cloudinary,
|
|
12
|
+
}) => {
|
|
13
|
+
const renderAssets = assets.filter(Boolean).map((asset, index) => (
|
|
14
|
+
<>
|
|
15
|
+
<Asset
|
|
16
|
+
key={asset.sys?.id}
|
|
17
|
+
asset={asset}
|
|
18
|
+
index={index}
|
|
19
|
+
sdk={sdk}
|
|
20
|
+
cloudinary={cloudinary}
|
|
21
|
+
assetsLength={assets.length}
|
|
22
|
+
onMoveClick={(direction) => handleMoveClick(direction, index)}
|
|
23
|
+
onDeleteClick={() => handleDeleteClick(index)}
|
|
24
|
+
/>
|
|
25
|
+
{index !== assets.length - 1 && <hr className="my-2" style={{borderColor: "#cfd9e0"}}/>}
|
|
26
|
+
</>
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
const handleMoveClick = (direction, index) => {
|
|
30
|
+
let newIndex = direction === "up" ? index - 1 : index + 1;
|
|
31
|
+
|
|
32
|
+
if (direction === "up" && index === 0) {
|
|
33
|
+
return;
|
|
34
|
+
} else if (direction === "down" && index === assets.length - 1) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChange(swapArrayElements(assets, index, newIndex));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleDeleteClick = (index) => {
|
|
42
|
+
let newAssets = assets;
|
|
43
|
+
|
|
44
|
+
newAssets.splice(index, 1);
|
|
45
|
+
|
|
46
|
+
onChange(newAssets);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>{renderAssets}</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Asset = ({
|
|
55
|
+
asset,
|
|
56
|
+
index,
|
|
57
|
+
assetsLength,
|
|
58
|
+
onDeleteClick,
|
|
59
|
+
onMoveClick,
|
|
60
|
+
sdk,
|
|
61
|
+
cloudinary,
|
|
62
|
+
}) => {
|
|
63
|
+
const [mediaUrl, setMediaUrl] = useState(null);
|
|
64
|
+
|
|
65
|
+
let id = asset?.sys?.id;
|
|
66
|
+
let title = asset?.fields?.title?.["en-US"] || asset?.fields?.title?.["de-DE"];
|
|
67
|
+
let space = asset?.sys?.space?.sys?.id;
|
|
68
|
+
let environment = asset?.sys?.environment?.sys?.id;
|
|
69
|
+
let mediaAssetId = asset?.fields?.media?.["de-DE"]?.sys?.id || asset?.fields?.media?.["en-US"]?.sys?.id;
|
|
70
|
+
let href = `https://app.contentful.com/spaces/${space}/environments/${environment}/entries/${id}`;
|
|
71
|
+
|
|
72
|
+
if (cloudinary) {
|
|
73
|
+
title = asset.public_id;
|
|
74
|
+
href = asset.url;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (cloudinary) {
|
|
79
|
+
setMediaUrl(asset.url);
|
|
80
|
+
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!mediaAssetId) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sdk.space.getAsset(mediaAssetId)
|
|
89
|
+
.then(a => {
|
|
90
|
+
let url = a?.fields?.file?.["de-DE"]?.url || a?.fields?.file?.["en-US"]?.url;
|
|
91
|
+
|
|
92
|
+
if (url) {
|
|
93
|
+
let params = new URLSearchParams({
|
|
94
|
+
w: "512",
|
|
95
|
+
q: "80",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
setMediaUrl(`${url}?${params}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}, [mediaAssetId, sdk, asset, cloudinary]);
|
|
102
|
+
|
|
103
|
+
if (!asset) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex flex-col">
|
|
109
|
+
<div className="flex">
|
|
110
|
+
<div className="grow">
|
|
111
|
+
<b className="mt-[4px] block break-all">{title}</b>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="ml-2 shrink-0">
|
|
114
|
+
{!!onMoveClick && (
|
|
115
|
+
<div className="icon-button-group mr-1">
|
|
116
|
+
<IconButton size="small" onClick={() => onMoveClick("up")} disabled={index === 0}>↑</IconButton>
|
|
117
|
+
<IconButton size="small" onClick={() => onMoveClick("down")} disabled={index === assetsLength - 1}>↓</IconButton>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
<a className="mr-1" target="_blank" href={href} rel="noreferrer">
|
|
121
|
+
<IconButton size="small">View</IconButton>
|
|
122
|
+
</a>
|
|
123
|
+
<IconButton size="small" onClick={onDeleteClick}>⨯</IconButton>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
{!!mediaUrl && <img src={mediaUrl} width="100%" className="mt-2 rounded-md" />}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|