@vonaffenfels/slate-editor 1.0.60 → 1.0.63

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.63",
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": "fe0fcf870d95e0f1c3cc6e8edd31a4c70465df63",
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;
@@ -156,4 +156,24 @@
156
156
  .message.message--negative {
157
157
  background-color: rgba(238, 26, 26, 0.2);
158
158
  }
159
+
160
+ .collapsable-menu {
161
+ background-color: white !important;
162
+ border: 1px solid #cfd9e0 !important;
163
+ border-radius: 6px;
164
+ z-index: 2;
165
+ overflow: hidden;
166
+ }
167
+
168
+ .collapsable-menu-item {
169
+ color: rgb(90, 101, 124) !important;
170
+ }
171
+
172
+ .collapsable-menu-item:not(.disabled):hover {
173
+ background-color: rgb(248, 248, 248) !important;
174
+ }
175
+
176
+ .collapsable-menu-item:not(.disabled):active {
177
+ background-color: rgb(237, 237, 237) !important;
178
+ }
159
179
  }
@@ -204,7 +204,7 @@ export default function BlockEditor({
204
204
  setSelectedStorybookElement(mergedValue);
205
205
  };
206
206
 
207
- const handleSidebarDeleteClick = (element) => {
207
+ const handleSidebarDeleteClick = () => {
208
208
  contentfulSdk.dialogs
209
209
  .openConfirm({
210
210
  title: 'Element löschen',
@@ -271,6 +271,50 @@ export default function BlockEditor({
271
271
  }
272
272
  };
273
273
 
274
+ const handleSidebarDuplicateClick = () => {
275
+ let path = selectedStorybookElement.path;
276
+
277
+ let selectedStorybookElementCopy = JSON.parse(JSON.stringify({
278
+ ...selectedStorybookElement,
279
+ editorAttributes: null,
280
+ path: null,
281
+ node: null,
282
+ }));
283
+
284
+ delete selectedStorybookElementCopy.path;
285
+ delete selectedStorybookElementCopy.node;
286
+ delete selectedStorybookElementCopy.editorAttributes;
287
+
288
+ try {
289
+ Transforms.insertNodes(editor, [selectedStorybookElementCopy], {at: path});
290
+ } catch (e) {
291
+ console.error(e);
292
+ }
293
+ };
294
+
295
+ const handleSidebarInsertClick = direction => {
296
+ let node = ReactEditor.toSlateNode(editor, selectedStorybookElement.editorAttributes.ref.current);
297
+ let path = ReactEditor.findPath(editor, node);
298
+
299
+ let at;
300
+
301
+ if (direction === "above") {
302
+ at = [path[0]];
303
+ } else {
304
+ at = [path[0] + 1];
305
+ }
306
+
307
+ Transforms.insertNodes(editor, [
308
+ {
309
+ children: [{text: ''}],
310
+ block: "Blocks/Empty",
311
+ attributes: {blockWidth: "site"},
312
+ isEmpty: true,
313
+ type: "storybook",
314
+ },
315
+ ], {at});
316
+ };
317
+
274
318
  const handleSidebarClose = () => {
275
319
  setSelectedStorybookElement(null);
276
320
  };
@@ -337,7 +381,10 @@ export default function BlockEditor({
337
381
  onClose={handleSidebarClose}
338
382
  onDelete={handleSidebarDeleteClick}
339
383
  onMove={handleSidebarMoveClick}
340
- editor={editor}/>
384
+ onDuplicate={handleSidebarDuplicateClick}
385
+ onInsert={handleSidebarInsertClick}
386
+ editor={editor}
387
+ lastSelection={lastSelection} />
341
388
  }
342
389
  />
343
390
  </div>
@@ -0,0 +1,46 @@
1
+ import {useState} from "react";
2
+
3
+ export const CollapsableMenu = ({
4
+ button,
5
+ children,
6
+ ...props
7
+ }) => {
8
+ const [collapsed, setCollapsed] = useState(true);
9
+
10
+ children = children.map(child => ({
11
+ ...child,
12
+ props: {
13
+ ...child.props,
14
+ onClick: () => {
15
+ child.props.onClick();
16
+ setCollapsed(true);
17
+ },
18
+ },
19
+ }));
20
+
21
+ return (
22
+ <div className="relative">
23
+ <div className="relative">
24
+ <div onClick={() => setCollapsed(!collapsed)}>
25
+ {button}
26
+ </div>
27
+ </div>
28
+ {!collapsed && (
29
+ <div className="collapsable-menu absolute right-0 mt-2 w-52">
30
+ {children}
31
+ </div>
32
+ )}
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export const CollapsableMenuItem = ({
38
+ children,
39
+ onClick,
40
+ }) => {
41
+ return (
42
+ <div className="collapsable-menu-item cursor-pointer px-4 py-2" onClick={onClick}>
43
+ {children}
44
+ </div>
45
+ );
46
+ };
@@ -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
+ };
@@ -1,12 +1,14 @@
1
1
  import {
2
- Fragment, useState, useEffect,
2
+ useState, useEffect,
3
3
  } from "react";
4
4
  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
-
9
- const devMode = localStorage.getItem("dev-mode") === "true";
8
+ import {ElementAutocomplete} from "./ElementAutocomplete";
9
+ import {
10
+ CollapsableMenu, CollapsableMenuItem,
11
+ } from "./CollapsableMenu/CollapsableMenu";
10
12
 
11
13
  const SidebarEditor = ({
12
14
  sdk,
@@ -16,9 +18,14 @@ const SidebarEditor = ({
16
18
  onChange,
17
19
  onDelete,
18
20
  onMove,
21
+ onDuplicate,
22
+ onInsert,
19
23
  editor,
20
24
  isLoading,
25
+ lastSelection,
21
26
  }) => {
27
+ const portal = sdk?.entry?.fields?.portal.getValue();
28
+
22
29
  const [versions, setVersions] = useState([]);
23
30
  const [lastChangedProperty, setLastChangedProperty] = useState(null);
24
31
  const [versionCount, setVersionCount] = useState(0);
@@ -164,8 +171,33 @@ const SidebarEditor = ({
164
171
  }
165
172
  };
166
173
 
174
+ const handleAutocompleteChange = (item) => {
175
+ if (!item) {
176
+ return onChange({}); // reset to empty
177
+ }
178
+
179
+ // reset to the first or second story
180
+ onChange({
181
+ ...storybookElement,
182
+ block: item.value,
183
+ attributes: {...(item?.stories?.[0]?.args || item?.stories?.[1]?.args || {})},
184
+ });
185
+ };
186
+
167
187
  useEffect(() => resetVersions, [storybookElement?.editorAttributes?.ref]);
168
188
 
189
+ const renderTitle = () => {
190
+ const story = storybookStories?.find(v => storybookElement.block?.toLowerCase() === v.id?.toLowerCase());
191
+
192
+ if (!story?.title) {
193
+ return null;
194
+ }
195
+
196
+ let storyTitleSplit = String(story.title || "").split("/");
197
+
198
+ return <h2 className="mb-2 text-lg font-bold">{storyTitleSplit[storyTitleSplit.length - 1]}</h2>;
199
+ };
200
+
169
201
  return (
170
202
  <div id="sidebar-editor-wrapper">
171
203
  <div id="sidebar-editor">
@@ -198,23 +230,35 @@ const SidebarEditor = ({
198
230
 
199
231
  </IconButton>
200
232
  </div>
201
- <IconButton title="Löschen" onClick={() => onDelete && onDelete(storybookElement)}>
233
+ <IconButton title="Löschen" className="mr-1" onClick={() => onDelete && onDelete(storybookElement)}>
202
234
  🗑
203
235
  </IconButton>
236
+ <CollapsableMenu button={<IconButton title="Funktionen">…</IconButton>}>
237
+ <CollapsableMenuItem onClick={onDuplicate}>Duplizieren</CollapsableMenuItem>
238
+ <CollapsableMenuItem onClick={() => onInsert("above")}>Davor hinzufügen</CollapsableMenuItem>
239
+ <CollapsableMenuItem onClick={() => onInsert("below")}>Danach hinzufügen</CollapsableMenuItem>
240
+ </CollapsableMenu>
204
241
  </div>
205
242
  )}
206
243
  </div>
207
- {!!onClose && <IconButton size="big" onClick={onClose} title="Schließen">⨯</IconButton>}
244
+ {!!onClose && <IconButton onClick={onClose} title="Schließen">⨉</IconButton>}
208
245
  </div>
209
246
  <hr className="mt-2" style={{borderColor: "#cfd9e0"}}/>
210
247
  </div>
211
248
  <div className="grow overflow-y-auto pr-2 pt-2">
249
+ {renderTitle()}
212
250
  <div className="mb-2">
213
- <BlockSelect
214
- stories={storybookStories}
215
- active={storybookElement}
216
- onChange={handleBlockSelectChange}
217
- storyContext={sdk.parameters.instance.storyContext} />
251
+ <ElementAutocomplete
252
+ isLoading={isLoading}
253
+ storybookStories={storybookStories}
254
+ editor={editor}
255
+ storyContext={sdk.parameters.instance.storyContext}
256
+ portal={portal}
257
+ lastSelection={lastSelection}
258
+ onChange={handleAutocompleteChange}
259
+ placeholder="Element wählen"
260
+ width="full"
261
+ />
218
262
  </div>
219
263
  {storybookElement?.block && (
220
264
  <div>
@@ -285,6 +329,7 @@ export const IconButton = ({
285
329
  disabled,
286
330
  size = "medium",
287
331
  className,
332
+ onClick = () => {},
288
333
  ...props
289
334
  }) => {
290
335
  let classNames = "icon-button cursor-pointer select-none !p-1";
@@ -310,7 +355,7 @@ export const IconButton = ({
310
355
  }
311
356
 
312
357
  return (
313
- <div className={classNames} {...props}>
358
+ <div className={classNames} onClick={onClick} {...props}>
314
359
  {children}
315
360
  </div>
316
361
  );
@@ -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
+ // :)