@vonaffenfels/slate-editor 1.0.60 → 1.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonaffenfels/slate-editor",
3
- "version": "1.0.60",
3
+ "version": "1.0.62",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -71,7 +71,7 @@
71
71
  "cssnano": "^5.0.1",
72
72
  "escape-html": "^1.0.3"
73
73
  },
74
- "gitHead": "82ea53774b0e6a0f904cfaaca038bf0eda00af8e",
74
+ "gitHead": "6ad3de5f5d37e2ca7447e7591f8f9e4d32089f30",
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  }
package/scss/editor.scss CHANGED
@@ -397,15 +397,9 @@
397
397
  }
398
398
 
399
399
  .button.button--secondary {
400
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
401
- width: 100%;
402
- padding: 8px 0.75rem !important;
403
400
  color: #036fe3 !important;
404
401
  border: 1px solid #036fe3 !important;
405
- transition: all 100ms ease-in-out;
406
402
  background-color: transparent !important;
407
- font-size: 0.875rem !important;
408
- border-radius: 4px !important;
409
403
  }
410
404
 
411
405
  .button.button--secondary:hover {
@@ -418,6 +412,20 @@
418
412
  color: #FFFFFF !important;
419
413
  }
420
414
 
415
+ .button.button--tertiary {
416
+ color: rgb(90, 101, 124) !important;
417
+ background-color: white !important;
418
+ border: 1px solid #cfd9e0 !important;
419
+ }
420
+
421
+ .button.button--tertiary:hover {
422
+ background-color: rgb(248, 248, 248) !important;
423
+ }
424
+
425
+ .button.button--tertiary:active {
426
+ background-color: rgb(237, 237, 237) !important;
427
+ }
428
+
421
429
  .resizable {
422
430
  .resizable-left {
423
431
  position: relative;
@@ -93,6 +93,32 @@ export default function BlockEditor({
93
93
  loadStories();
94
94
  }, []);
95
95
 
96
+ const handleKeyDown = (e) => {
97
+ let keyCode = e.keyCode || e.which;
98
+
99
+ switch (keyCode) {
100
+ case 46: // delete
101
+ case 8: // backspace
102
+ handleSidebarDeleteClick();
103
+ break;
104
+ case 27: // escape
105
+ handleSidebarClose();
106
+ break;
107
+ default:
108
+ return;
109
+ }
110
+ };
111
+
112
+ useEffect(() => {
113
+ if (selectedStorybookElement) {
114
+ window.addEventListener("keydown", handleKeyDown);
115
+ } else {
116
+ window.removeEventListener("keydown", handleKeyDown);
117
+ }
118
+
119
+ return () => window.removeEventListener("keydown", handleKeyDown);
120
+ }, [selectedStorybookElement]);
121
+
96
122
  const resetEditor = () => {
97
123
  if (confirm("This action will delete all data in the editor, are you sure?")) {
98
124
  onChange(emptyValue);
@@ -204,7 +230,7 @@ export default function BlockEditor({
204
230
  setSelectedStorybookElement(mergedValue);
205
231
  };
206
232
 
207
- const handleSidebarDeleteClick = (element) => {
233
+ const handleSidebarDeleteClick = () => {
208
234
  contentfulSdk.dialogs
209
235
  .openConfirm({
210
236
  title: 'Element löschen',
@@ -271,6 +297,50 @@ export default function BlockEditor({
271
297
  }
272
298
  };
273
299
 
300
+ const handleSidebarDuplicateClick = () => {
301
+ let path = selectedStorybookElement.path;
302
+
303
+ let selectedStorybookElementCopy = JSON.parse(JSON.stringify({
304
+ ...selectedStorybookElement,
305
+ editorAttributes: null,
306
+ path: null,
307
+ node: null,
308
+ }));
309
+
310
+ delete selectedStorybookElementCopy.path;
311
+ delete selectedStorybookElementCopy.node;
312
+ delete selectedStorybookElementCopy.editorAttributes;
313
+
314
+ try {
315
+ Transforms.insertNodes(editor, [selectedStorybookElementCopy], {at: path});
316
+ } catch (e) {
317
+ console.error(e);
318
+ }
319
+ };
320
+
321
+ const handleSidebarInsertClick = direction => {
322
+ let node = ReactEditor.toSlateNode(editor, selectedStorybookElement.editorAttributes.ref.current);
323
+ let path = ReactEditor.findPath(editor, node);
324
+
325
+ let at;
326
+
327
+ if (direction === "above") {
328
+ at = [path[0]];
329
+ } else {
330
+ at = [path[0] + 1];
331
+ }
332
+
333
+ Transforms.insertNodes(editor, [
334
+ {
335
+ children: [{text: ''}],
336
+ block: "Blocks/Empty",
337
+ attributes: {blockWidth: "site"},
338
+ isEmpty: true,
339
+ type: "storybook",
340
+ },
341
+ ], {at});
342
+ };
343
+
274
344
  const handleSidebarClose = () => {
275
345
  setSelectedStorybookElement(null);
276
346
  };
@@ -337,7 +407,10 @@ export default function BlockEditor({
337
407
  onClose={handleSidebarClose}
338
408
  onDelete={handleSidebarDeleteClick}
339
409
  onMove={handleSidebarMoveClick}
340
- editor={editor}/>
410
+ onDuplicate={handleSidebarDuplicateClick}
411
+ onInsert={handleSidebarInsertClick}
412
+ editor={editor}
413
+ lastSelection={lastSelection} />
341
414
  }
342
415
  />
343
416
  </div>
@@ -0,0 +1,84 @@
1
+ import {Autocomplete} from "@contentful/forma-36-react-components";
2
+ import {
3
+ useEffect, useState,
4
+ } from "react";
5
+ import {Transforms} from "slate";
6
+
7
+ const devMode = localStorage.getItem("dev-mode") === "true";
8
+
9
+ export const ElementAutocomplete = ({
10
+ storybookStories,
11
+ isLoading,
12
+ editor,
13
+ storyContext = "",
14
+ portal,
15
+ lastSelection,
16
+ onChange,
17
+ ...props
18
+ }) => {
19
+ const items = (storybookStories || []).map(story => {
20
+ let storyTitleSplit = String(story.title || "").split("/");
21
+
22
+ if (!story.id) {
23
+ return;
24
+ }
25
+
26
+ let splitStoryContext = String(storyContext || "").split(",");
27
+ let isItemInStoryContext = splitStoryContext.find(context => {
28
+ return Array.isArray(story.storyContext) ? story.storyContext.includes(context) : context === story.storyContext;
29
+ });
30
+ let isItemInPortalContext = !story.portalContext || story.portalContext.includes(portal);
31
+
32
+ if (!devMode && (!isItemInStoryContext || !isItemInPortalContext)) {
33
+ return;
34
+ }
35
+
36
+ return {
37
+ value: story.id.toLowerCase(),
38
+ label: storyTitleSplit[storyTitleSplit.length - 1],
39
+ stories: story.stories,
40
+ };
41
+ }).filter(Boolean);
42
+
43
+ const [filteredItems, setFilteredItems] = useState(items);
44
+
45
+ useEffect(() => {
46
+ setFilteredItems(items);
47
+ }, [storybookStories]);
48
+
49
+ const handleQueryChange = (query) => {
50
+ setFilteredItems(query ? items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())) : items);
51
+ };
52
+
53
+ const handleOnChange = (item) => {
54
+ onChange(item);
55
+ };
56
+
57
+ useEffect(() => {
58
+ let autoCompleteElement = document.getElementsByClassName("element-autocomplete")[0];
59
+
60
+ if (autoCompleteElement) {
61
+ autoCompleteElement.value = "";
62
+ }
63
+ }, []);
64
+
65
+ return (
66
+ <Autocomplete
67
+ items={filteredItems}
68
+ onQueryChange={handleQueryChange}
69
+ isLoading={isLoading}
70
+ placeholder={'Element hinzufügen'}
71
+ emptyListMessage={'Keine Komponenten gefunden'}
72
+ noMatchesMessage={'Keine Ergebnisse gefunden'}
73
+ dropdownProps={{isFullWidth: true}}
74
+ maxHeight={300}
75
+ onChange={handleOnChange}
76
+ width="medium"
77
+ {...props}
78
+ >
79
+ {(options) =>
80
+ options.map((option) => <span key={option.value}>{option.label}</span>)
81
+ }
82
+ </Autocomplete>
83
+ );
84
+ };
@@ -1,6 +1,11 @@
1
- import {useEffect, useRef} from "react";
1
+ import {
2
+ useEffect, useRef,
3
+ } from "react";
2
4
 
3
- export const Resizable = ({left, right}) => {
5
+ export const Resizable = ({
6
+ left,
7
+ right,
8
+ }) => {
4
9
  const containerRef = useRef();
5
10
  const leftRef = useRef();
6
11
  const handleRef = useRef();
@@ -14,11 +19,11 @@ export const Resizable = ({left, right}) => {
14
19
  if (width < 300) {
15
20
  return 300;
16
21
  } else if (containerRef.current.getBoundingClientRect().width - width < 300) {
17
- return containerRef.current.getBoundingClientRect().width - 300
22
+ return containerRef.current.getBoundingClientRect().width - 300;
18
23
  }
19
24
 
20
25
  return width;
21
- }
26
+ };
22
27
 
23
28
  useEffect(() => {
24
29
  if (rightRef.current) {
@@ -41,18 +46,18 @@ export const Resizable = ({left, right}) => {
41
46
  return;
42
47
  }
43
48
 
44
- let newWidth = rightRef.current.resizingStartWidth - (e.pageX - rightRef.current.resizingStartMouseX)
49
+ let newWidth = rightRef.current.resizingStartWidth - (e.pageX - rightRef.current.resizingStartMouseX);
45
50
  newWidth = checkWidth(newWidth);
46
51
 
47
52
  localStorage.setItem("slate-editor-resizable-width", newWidth);
48
53
 
49
54
  rightRef.current.style.width = newWidth + 'px';
50
- }
55
+ };
51
56
  const onMouseUp = (e) => {
52
57
  if (rightRef.current) {
53
58
  rightRef.current.resizing = false;
54
59
  }
55
- }
60
+ };
56
61
 
57
62
  rightRef.current.addEventListener("mousedown", onMouseDown);
58
63
  document.addEventListener("mousemove", onMouseMove);
@@ -64,15 +69,15 @@ export const Resizable = ({left, right}) => {
64
69
  }
65
70
  document.removeEventListener("mousemove", onMouseMove);
66
71
  document.removeEventListener("mouseup", onMouseUp);
67
- }
72
+ };
68
73
  }
69
74
  }, [right, left]);
70
75
 
71
- return <div className="w-full flex resizable" ref={containerRef}>
72
- <div ref={leftRef} className="resizable-left flex-grow">{left}</div>
73
- {right && <div ref={rightRef} className="resizable-right">
76
+ return <div className="resizable flex w-full" ref={containerRef}>
77
+ <div ref={leftRef} className="resizable-left grow">{left}</div>
78
+ {right && <div ref={rightRef} className="resizable-right shrink-0">
74
79
  <div ref={handleRef} className="resizable-x-handle"/>
75
80
  {right}
76
81
  </div>}
77
- </div>
78
- }
82
+ </div>;
83
+ };
@@ -5,6 +5,7 @@ import {SidebarEditorField} from "./SidebarEditor/SidebarEditorField";
5
5
  import {ToolMargin} from "./Tools/Margin";
6
6
  import "../scss/sidebarEditor.scss";
7
7
  import {Spinner} from "@contentful/forma-36-react-components";
8
+ import {ElementAutocomplete} from "./ElementAutocomplete";
8
9
 
9
10
  const devMode = localStorage.getItem("dev-mode") === "true";
10
11
 
@@ -16,9 +17,14 @@ const SidebarEditor = ({
16
17
  onChange,
17
18
  onDelete,
18
19
  onMove,
20
+ onDuplicate,
21
+ onInsert,
19
22
  editor,
20
23
  isLoading,
24
+ lastSelection,
21
25
  }) => {
26
+ const portal = sdk?.entry?.fields?.portal.getValue();
27
+
22
28
  const [versions, setVersions] = useState([]);
23
29
  const [lastChangedProperty, setLastChangedProperty] = useState(null);
24
30
  const [versionCount, setVersionCount] = useState(0);
@@ -164,8 +170,33 @@ const SidebarEditor = ({
164
170
  }
165
171
  };
166
172
 
173
+ const handleAutocompleteChange = (item) => {
174
+ if (!item) {
175
+ return onChange({}); // reset to empty
176
+ }
177
+
178
+ // reset to the first or second story
179
+ onChange({
180
+ ...storybookElement,
181
+ block: item.value,
182
+ attributes: {...(item?.stories?.[0]?.args || item?.stories?.[1]?.args || {})},
183
+ });
184
+ };
185
+
167
186
  useEffect(() => resetVersions, [storybookElement?.editorAttributes?.ref]);
168
187
 
188
+ const renderTitle = () => {
189
+ const story = storybookStories?.find(v => storybookElement.block?.toLowerCase() === v.id?.toLowerCase());
190
+
191
+ if (!story?.title) {
192
+ return null;
193
+ }
194
+
195
+ let storyTitleSplit = String(story.title || "").split("/");
196
+
197
+ return <h2 className="mb-2 text-lg font-bold">{storyTitleSplit[storyTitleSplit.length - 1]}</h2>;
198
+ };
199
+
169
200
  return (
170
201
  <div id="sidebar-editor-wrapper">
171
202
  <div id="sidebar-editor">
@@ -204,17 +235,31 @@ const SidebarEditor = ({
204
235
  </div>
205
236
  )}
206
237
  </div>
207
- {!!onClose && <IconButton size="big" onClick={onClose} title="Schließen">⨯</IconButton>}
238
+ {!!onClose && <IconButton onClick={onClose} title="Schließen">⨉</IconButton>}
208
239
  </div>
209
240
  <hr className="mt-2" style={{borderColor: "#cfd9e0"}}/>
210
241
  </div>
211
242
  <div className="grow overflow-y-auto pr-2 pt-2">
243
+ {renderTitle()}
212
244
  <div className="mb-2">
213
- <BlockSelect
214
- stories={storybookStories}
215
- active={storybookElement}
216
- onChange={handleBlockSelectChange}
217
- storyContext={sdk.parameters.instance.storyContext} />
245
+ <button className="button button--tertiary" onClick={onDuplicate}>Duplizieren</button>
246
+ </div>
247
+ <div className="mb-2 flex">
248
+ <button className="button button--tertiary mr-1" onClick={() => onInsert("above")}>Davor hinzufügen</button>
249
+ <button className="button button--tertiary ml-1" onClick={() => onInsert("below")}>Danach hinzufügen</button>
250
+ </div>
251
+ <div className="mb-2">
252
+ <ElementAutocomplete
253
+ isLoading={isLoading}
254
+ storybookStories={storybookStories}
255
+ editor={editor}
256
+ storyContext={sdk.parameters.instance.storyContext}
257
+ portal={portal}
258
+ lastSelection={lastSelection}
259
+ onChange={handleAutocompleteChange}
260
+ placeholder="Element wählen"
261
+ width="full"
262
+ />
218
263
  </div>
219
264
  {storybookElement?.block && (
220
265
  <div>
@@ -367,108 +412,6 @@ const VariantSelect = ({
367
412
  );
368
413
  };
369
414
 
370
- const BlockSelect = ({
371
- stories,
372
- active,
373
- onChange,
374
- className,
375
- storyContext,
376
- }) => {
377
- if (!onChange) {
378
- return null;
379
- }
380
-
381
- const onSelectChange = (story) => {
382
- if (!story) {
383
- return onChange({}); // reset to empty
384
- }
385
-
386
- // reset to the first or second story
387
- onChange({
388
- ...active,
389
- block: story.id,
390
- attributes: {...(story?.stories?.[0]?.args || story?.stories?.[1]?.args || {})},
391
- });
392
- };
393
-
394
- function groupArrayToObject(array) {
395
- const result = {};
396
-
397
- array.forEach(item => {
398
- if (!item?.id) {
399
- return;
400
- }
401
-
402
- let splitStoryContext = String(storyContext || "").split(",");
403
- let isItemInContext = splitStoryContext.find(context => {
404
- return Array.isArray(item.storyContext) ? item.storyContext.includes(context) : context === item.storyContext;
405
- });
406
-
407
- if (!devMode && !isItemInContext) {
408
- return;
409
- }
410
-
411
- const parts = item.title.split('/');
412
- let currentLevel = result;
413
-
414
- parts.forEach((part, index) => {
415
- if (!currentLevel[part]) {
416
- currentLevel[part] = {level: index};
417
- }
418
- currentLevel = currentLevel[part];
419
- });
420
-
421
- currentLevel.title = item.title;
422
- currentLevel.shortTitle = parts[parts.length - 1];
423
- currentLevel.id = item.id;
424
- });
425
-
426
- return result;
427
- }
428
-
429
- let storyGroups = groupArrayToObject(stories);
430
-
431
- const renderOptions = (obj) => Object.keys(obj).map((key, index) => {
432
- const option = obj[key];
433
-
434
- if (!option) {
435
- return null;
436
- }
437
-
438
- if (option.title && option.id) {
439
- return (
440
- <option key={option.id} value={option.id.toLowerCase()}>
441
- {option.shortTitle}
442
- </option>
443
- );
444
- } else if (typeof option === "object") {
445
- return (
446
- <Fragment key={`${key}-${index}`}>
447
- <option
448
- disabled
449
- style={{
450
- color: "rgba(0, 0, 0, 0.3)",
451
- fontWeight: option.level === 0 ? "bold" : "normal",
452
- }}>{key}</option>
453
- {renderOptions(option)}
454
- </Fragment>
455
- );
456
- }
457
- });
458
-
459
- return (
460
- <div className={className}>
461
- <label className="block">Element</label>
462
- <select
463
- onChange={e => onSelectChange(stories.find(s => s.id?.toLowerCase() === e.target.value))}
464
- value={active?.block?.toLowerCase()}
465
- className="font-bold"
466
- >
467
- <option>Element wählen</option>
468
- {renderOptions(storyGroups)}
469
- </select>
470
- </div>
471
- );
472
- };
415
+ export default SidebarEditor;
473
416
 
474
- export default SidebarEditor;
417
+ // :)
@@ -25,8 +25,7 @@ import {
25
25
  Autocomplete, Spinner,
26
26
  } from "@contentful/forma-36-react-components";
27
27
  import {Transforms} from "slate";
28
-
29
- const devMode = localStorage.getItem("dev-mode") === "true";
28
+ import {ElementAutocomplete} from "../ElementAutocomplete";
30
29
 
31
30
  export const Portal = ({children}) => {
32
31
  return ReactDOM.createPortal(children, window.document.body);
@@ -95,6 +94,17 @@ export const Toolbar = ({
95
94
  }
96
95
  }, [hover, ref, editor]);
97
96
 
97
+ const handleAutocompleteChange = item => {
98
+ let element = {
99
+ children: [{text: ''}],
100
+ type: "storybook",
101
+ block: item.value,
102
+ attributes: {...(item?.stories?.[0]?.args || item?.stories?.[1]?.args || {})},
103
+ };
104
+
105
+ Transforms.insertNodes(editor, [element], {at: [lastSelection?.anchor?.path?.[0]]});
106
+ };
107
+
98
108
  function renderMenu() {
99
109
  return <Menu
100
110
  ref={ref}
@@ -149,6 +159,7 @@ export const Toolbar = ({
149
159
  storyContext={sdk.parameters.instance.storyContext}
150
160
  portal={portal}
151
161
  lastSelection={lastSelection}
162
+ onChange={handleAutocompleteChange}
152
163
  />
153
164
  </div>
154
165
  </div>
@@ -170,87 +181,6 @@ export const Toolbar = ({
170
181
  }
171
182
  };
172
183
 
173
- const ElementAutocomplete = ({
174
- storybookStories,
175
- isLoading,
176
- editor,
177
- storyContext = "",
178
- portal,
179
- lastSelection,
180
- }) => {
181
- const items = (storybookStories || []).map(story => {
182
- let storyTitleSplit = String(story.title || "").split("/");
183
-
184
- if (!story.id) {
185
- return;
186
- }
187
-
188
- let splitStoryContext = String(storyContext || "").split(",");
189
- let isItemInStoryContext = splitStoryContext.find(context => {
190
- return Array.isArray(story.storyContext) ? story.storyContext.includes(context) : context === story.storyContext;
191
- });
192
- let isItemInPortalContext = !story.portalContext || story.portalContext.includes(portal);
193
-
194
- if (!devMode && (!isItemInStoryContext || !isItemInPortalContext)) {
195
- return;
196
- }
197
-
198
- return {
199
- value: story.id.toLowerCase(),
200
- label: storyTitleSplit[storyTitleSplit.length - 1],
201
- stories: story.stories,
202
- };
203
- }).filter(Boolean);
204
-
205
- const [filteredItems, setFilteredItems] = useState(items);
206
-
207
- useEffect(() => {
208
- setFilteredItems(items);
209
- }, [storybookStories]);
210
-
211
- const handleQueryChange = (query) => {
212
- setFilteredItems(query ? items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())) : items);
213
- };
214
-
215
- const handleOnChange = (item) => {
216
- let element = {
217
- children: [{text: ''}],
218
- type: "storybook",
219
- block: item.value,
220
- attributes: {...(item?.stories?.[0]?.args || item?.stories?.[1]?.args || {})},
221
- };
222
-
223
- Transforms.insertNodes(editor, [element], {at: [lastSelection?.anchor?.path?.[0]]});
224
- };
225
-
226
- useEffect(() => {
227
- let autoCompleteElement = document.getElementsByClassName("element-autocomplete")[0];
228
-
229
- if (autoCompleteElement) {
230
- autoCompleteElement.value = "";
231
- }
232
- }, []);
233
-
234
- return (
235
- <Autocomplete
236
- items={filteredItems}
237
- onQueryChange={handleQueryChange}
238
- isLoading={isLoading}
239
- placeholder={'Element hinzufügen'}
240
- emptyListMessage={'Keine Komponenten gefunden'}
241
- noMatchesMessage={'Keine Ergebnisse gefunden'}
242
- dropdownProps={{isFullWidth: true}}
243
- maxHeight={300}
244
- onChange={handleOnChange}
245
- width="medium"
246
- >
247
- {(options) =>
248
- options.map((option) => <span key={option.value}>{option.label}</span>)
249
- }
250
- </Autocomplete>
251
- );
252
- };
253
-
254
184
  export const ToobarHoverExpandButton = ({children}) => {
255
185
  return <span
256
186
  className={