@vonaffenfels/slate-editor 1.0.2 → 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.
@@ -1,11 +1,11 @@
1
1
  import React, {
2
- useCallback, useMemo, useState, useRef, useContext, useEffect,
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 storybookContext = useContext(StorybookContext);
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) => setSelectedStorybookElement({
43
- ...element,
44
- editorAttributes: attributes,
45
- })}
46
- onElementClick={() => setSelectedStorybookElement(null)}/>;
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
- let node = ReactEditor.toSlateNode(editor, element.editorAttributes.ref.current);
163
- let path = ReactEditor.findPath(editor, node);
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
- Transforms.setNodes(editor, {isDeleted: true}, {at: path});
166
- Transforms.removeNodes(editor, {at: path});
200
+ Transforms.setNodes(editor, {isDeleted: true}, {at: path});
201
+ Transforms.removeNodes(editor, {at: path});
167
202
 
168
- setSelectedStorybookElement(null);
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
- // Initially up
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) {
217
+ to = [path[0] + 1];
218
+ } else if (direction === "up" && path[0] > 0) {
179
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
- "block-editor-wrapper": true,
200
- "storybook-active": isStorybookActive,
201
- })}>
202
- <div className="storybook-target" ref={storybookTarget}/>
203
- <div className="slate-editor">
204
- <StorybookContext.Provider value={{
205
- ...storybookContext,
206
- target: storybookTarget,
207
- contentfulSdk: contentfulSdk,
208
- setActive: setStorybookActive,
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
- </Slate>
240
- </StorybookContext.Provider>
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={() => setSelectedStorybookElement(null)}
248
- onDelete={element => handleSidebarDeleteClick(element)}
249
- onMove={(element, direction) => handleSidebarMoveClick(element, direction)} />}
287
+ onClose={handleSidebarClose}
288
+ onDelete={handleSidebarDeleteClick}
289
+ onMove={handleSidebarMoveClick}/>}
250
290
  </div>
251
291
  );
252
292
  }
@@ -36,15 +36,17 @@ export const Element = ({
36
36
 
37
37
  switch (props.element.type) {
38
38
  case 'storybook':
39
- return <StorybookNode
40
- {...props}
41
- {...typeProps}
42
- isInSlot={isInSlot}
43
- storybookComponentLoader={storybookComponentLoader}
44
- storybookComponentDataLoader={storybookComponentDataLoader}
45
- editor={editor}
46
- onStorybookElementClick={onStorybookElementClick}
47
- >{children}</StorybookNode>;
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}
@@ -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="options-container">
123
- <span className="options-container-option options-container-option-edit" title="Edit" onClick={onEditClick}><FontAwesomeIcon
124
- icon={faCog}
125
- size="lg"/></span>
126
- {!element.isInline && (<>
127
- <span className="options-container-option options-container-option-move-up" title="Move up" onClick={moveUp}>
128
- <FontAwesomeIcon icon={faArrowUp} size="lg"/>
129
- </span>
130
- <span className="options-container-option options-container-option-move-down" title="Move down" onClick={moveDown}>
131
- <FontAwesomeIcon icon={faArrowDown} size="lg"/>
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
+ };