@vonaffenfels/slate-editor 1.0.59 → 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.59",
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": "45e9d0390c7c19fbfa54c8bf9c3cef5a076e4198",
74
+ "gitHead": "6ad3de5f5d37e2ca7447e7591f8f9e4d32089f30",
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  }
package/scss/editor.scss CHANGED
@@ -259,8 +259,8 @@
259
259
 
260
260
  .layout-slot-option-padding {
261
261
  position: relative;
262
- width: 1.5em;
263
- height: 1.5em;
262
+ width: 26px;
263
+ height: 26px;
264
264
  display: inline-block;
265
265
 
266
266
  .layout-slot-option-padding-item {
@@ -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;
@@ -77,6 +77,7 @@ export default function BlockEditor({
77
77
 
78
78
  const [loadedStorybookStories, setLoadedStorybookStories] = useState([]);
79
79
  const [isLoadingStories, setIsLoadingStories] = useState(false);
80
+ const [lastSelection, setLastSelection] = useState(null);
80
81
 
81
82
  const loadStories = async () => {
82
83
  setIsLoadingStories(true);
@@ -92,6 +93,32 @@ export default function BlockEditor({
92
93
  loadStories();
93
94
  }, []);
94
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
+
95
122
  const resetEditor = () => {
96
123
  if (confirm("This action will delete all data in the editor, are you sure?")) {
97
124
  onChange(emptyValue);
@@ -99,6 +126,10 @@ export default function BlockEditor({
99
126
  };
100
127
 
101
128
  const onSlateChange = (newValue) => {
129
+ if (editor.selection) {
130
+ setLastSelection(editor.selection);
131
+ }
132
+
102
133
  if (newValue.length > 0) {
103
134
  onChange(newValue);
104
135
  } else {
@@ -199,7 +230,7 @@ export default function BlockEditor({
199
230
  setSelectedStorybookElement(mergedValue);
200
231
  };
201
232
 
202
- const handleSidebarDeleteClick = (element) => {
233
+ const handleSidebarDeleteClick = () => {
203
234
  contentfulSdk.dialogs
204
235
  .openConfirm({
205
236
  title: 'Element löschen',
@@ -266,6 +297,50 @@ export default function BlockEditor({
266
297
  }
267
298
  };
268
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
+
269
344
  const handleSidebarClose = () => {
270
345
  setSelectedStorybookElement(null);
271
346
  };
@@ -286,7 +361,8 @@ export default function BlockEditor({
286
361
  storybookStories={loadedStorybookStories}
287
362
  isLoadingStories={isLoadingStories}
288
363
  editor={editor}
289
- sdk={contentfulSdk}/>
364
+ sdk={contentfulSdk}
365
+ lastSelection={lastSelection}/>
290
366
  </div>
291
367
  <div className="relative h-full max-h-full overflow-scroll px-8 py-4" ref={scrollContainer}>
292
368
  {isLoading && (
@@ -331,7 +407,10 @@ export default function BlockEditor({
331
407
  onClose={handleSidebarClose}
332
408
  onDelete={handleSidebarDeleteClick}
333
409
  onMove={handleSidebarMoveClick}
334
- editor={editor}/>
410
+ onDuplicate={handleSidebarDuplicateClick}
411
+ onInsert={handleSidebarInsertClick}
412
+ editor={editor}
413
+ lastSelection={lastSelection} />
335
414
  }
336
415
  />
337
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
+ };
@@ -55,7 +55,7 @@ export const SidebarEditorField = ({
55
55
  ));
56
56
  } else {
57
57
  return Object.keys(field.control.options).map(key => (
58
- <option key={`select-option-${key}`} value={field.control.options[key]}>{field.control.options[key]}</option>
58
+ <option key={`select-option-${key}`} value={field.control.options[key]}>{key}</option>
59
59
  ));
60
60
  }
61
61
  };
@@ -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">
@@ -181,6 +212,11 @@ const SidebarEditor = ({
181
212
  <div className="grow">
182
213
  {storybookElement?.block && (
183
214
  <div className="flex items-center">
215
+ <div className="mr-1 flex items-center">
216
+ <ToolMargin
217
+ margin={storybookElement.attributes.margin}
218
+ onChange={value => handleFieldValueChange("margin", value)} />
219
+ </div>
184
220
  <div className="icon-button-group mr-1">
185
221
  <IconButton title="Rückgängig" onClick={undo} disabled={currentVersion <= 1}>↺</IconButton>
186
222
  <IconButton title="Wiederherstellen" onClick={redo} disabled={currentVersion >= versionCount || versionCount === 0}>↻</IconButton>
@@ -196,25 +232,34 @@ const SidebarEditor = ({
196
232
  <IconButton title="Löschen" onClick={() => onDelete && onDelete(storybookElement)}>
197
233
  🗑
198
234
  </IconButton>
199
- <div className="ml-1 flex items-center">
200
- <ToolMargin
201
- margin={storybookElement.attributes.margin}
202
- onChange={value => handleFieldValueChange("margin", value)} />
203
- </div>
204
235
  </div>
205
236
  )}
206
237
  </div>
207
- {!!onClose && <IconButton 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()}
244
+ <div className="mb-2">
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>
212
251
  <div className="mb-2">
213
- <BlockSelect
214
- stories={storybookStories}
215
- active={storybookElement}
216
- onChange={handleBlockSelectChange}
217
- storyContext={sdk.parameters.instance.storyContext} />
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>
@@ -299,11 +344,14 @@ export const IconButton = ({
299
344
 
300
345
  switch (size) {
301
346
  case "small":
302
- classNames += " !text-xs min-w-[28px]";
347
+ classNames += " !text-xs min-w-[28px] max-h-[28px]";
348
+ break;
349
+ case "big":
350
+ classNames += " !text-xl min-w-[32px] max-h-[28px] !leading-[18px]";
303
351
  break;
304
352
  case "medium":
305
353
  default:
306
- classNames += " !text-sm min-w-[32px]";
354
+ classNames += " !text-sm min-w-[32px] max-h-[28px]";
307
355
  }
308
356
 
309
357
  return (
@@ -364,108 +412,6 @@ const VariantSelect = ({
364
412
  );
365
413
  };
366
414
 
367
- const BlockSelect = ({
368
- stories,
369
- active,
370
- onChange,
371
- className,
372
- storyContext,
373
- }) => {
374
- if (!onChange) {
375
- return null;
376
- }
377
-
378
- const onSelectChange = (story) => {
379
- if (!story) {
380
- return onChange({}); // reset to empty
381
- }
382
-
383
- // reset to the first or second story
384
- onChange({
385
- ...active,
386
- block: story.id,
387
- attributes: {...(story?.stories?.[0]?.args || story?.stories?.[1]?.args || {})},
388
- });
389
- };
390
-
391
- function groupArrayToObject(array) {
392
- const result = {};
393
-
394
- array.forEach(item => {
395
- if (!item?.id) {
396
- return;
397
- }
398
-
399
- let splitStoryContext = String(storyContext || "").split(",");
400
- let isItemInContext = splitStoryContext.find(context => {
401
- return Array.isArray(item.storyContext) ? item.storyContext.includes(context) : context === item.storyContext;
402
- });
403
-
404
- if (!devMode && !isItemInContext) {
405
- return;
406
- }
407
-
408
- const parts = item.title.split('/');
409
- let currentLevel = result;
410
-
411
- parts.forEach((part, index) => {
412
- if (!currentLevel[part]) {
413
- currentLevel[part] = {level: index};
414
- }
415
- currentLevel = currentLevel[part];
416
- });
417
-
418
- currentLevel.title = item.title;
419
- currentLevel.shortTitle = parts[parts.length - 1];
420
- currentLevel.id = item.id;
421
- });
422
-
423
- return result;
424
- }
425
-
426
- let storyGroups = groupArrayToObject(stories);
427
-
428
- const renderOptions = (obj) => Object.keys(obj).map((key, index) => {
429
- const option = obj[key];
430
-
431
- if (!option) {
432
- return null;
433
- }
434
-
435
- if (option.title && option.id) {
436
- return (
437
- <option key={option.id} value={option.id.toLowerCase()}>
438
- {option.shortTitle}
439
- </option>
440
- );
441
- } else if (typeof option === "object") {
442
- return (
443
- <Fragment key={`${key}-${index}`}>
444
- <option
445
- disabled
446
- style={{
447
- color: "rgba(0, 0, 0, 0.3)",
448
- fontWeight: option.level === 0 ? "bold" : "normal",
449
- }}>{key}</option>
450
- {renderOptions(option)}
451
- </Fragment>
452
- );
453
- }
454
- });
455
-
456
- return (
457
- <div className={className}>
458
- <label className="block">Element</label>
459
- <select
460
- onChange={e => onSelectChange(stories.find(s => s.id?.toLowerCase() === e.target.value))}
461
- value={active?.block?.toLowerCase()}
462
- className="font-bold"
463
- >
464
- <option>Element wählen</option>
465
- {renderOptions(storyGroups)}
466
- </select>
467
- </div>
468
- );
469
- };
415
+ export default SidebarEditor;
470
416
 
471
- export default SidebarEditor;
417
+ // :)