@vonaffenfels/slate-editor 1.0.1

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.
Files changed (77) hide show
  1. package/.babelrc +44 -0
  2. package/README.md +5 -0
  3. package/componentLoader.js +52 -0
  4. package/dist/BlockEditor.css +93 -0
  5. package/dist/BlockEditor.js +2 -0
  6. package/dist/BlockEditor.js.LICENSE.txt +61 -0
  7. package/dist/Renderer.js +2 -0
  8. package/dist/Renderer.js.LICENSE.txt +15 -0
  9. package/dist/fromHTML.js +1 -0
  10. package/dist/index.css +93 -0
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.LICENSE.txt +69 -0
  13. package/dist/toHTML.js +2 -0
  14. package/dist/toHTML.js.LICENSE.txt +23 -0
  15. package/dist/toText.js +2 -0
  16. package/dist/toText.js.LICENSE.txt +23 -0
  17. package/package.json +79 -0
  18. package/postcss.config.js +7 -0
  19. package/scss/demo.scss +142 -0
  20. package/scss/editor.scss +394 -0
  21. package/scss/storybook.scss +66 -0
  22. package/scss/toolbar.scss +160 -0
  23. package/src/BlockEditor.js +252 -0
  24. package/src/Blocks/EmptyBlock.js +12 -0
  25. package/src/Blocks/EmptyWrapper.js +5 -0
  26. package/src/Blocks/ErrorBoundary.js +41 -0
  27. package/src/Blocks/LayoutBlock.js +179 -0
  28. package/src/Blocks/LayoutSlot.js +61 -0
  29. package/src/Context/StorybookContext.js +7 -0
  30. package/src/Nodes/Default.js +151 -0
  31. package/src/Nodes/Element.js +72 -0
  32. package/src/Nodes/Leaf.js +40 -0
  33. package/src/Nodes/Storybook.js +170 -0
  34. package/src/Nodes/StorybookDisplay.js +118 -0
  35. package/src/Nodes/Text.js +67 -0
  36. package/src/Renderer.js +41 -0
  37. package/src/Serializer/Html.js +43 -0
  38. package/src/Serializer/Serializer.js +318 -0
  39. package/src/Serializer/Text.js +18 -0
  40. package/src/Serializer/ads.js +175 -0
  41. package/src/Serializer/index.js +4 -0
  42. package/src/SidebarEditor/SidebarEditorField.js +249 -0
  43. package/src/SidebarEditor.css +90 -0
  44. package/src/SidebarEditor.js +236 -0
  45. package/src/Storybook.js +152 -0
  46. package/src/Toolbar/Align.js +65 -0
  47. package/src/Toolbar/Block.js +121 -0
  48. package/src/Toolbar/Element.js +49 -0
  49. package/src/Toolbar/Formats.js +60 -0
  50. package/src/Toolbar/Insert.js +29 -0
  51. package/src/Toolbar/Layout.js +333 -0
  52. package/src/Toolbar/Link.js +165 -0
  53. package/src/Toolbar/Toolbar.js +164 -0
  54. package/src/Tools/Margin.js +52 -0
  55. package/src/dev/App.js +61 -0
  56. package/src/dev/draftToSlate.json +3148 -0
  57. package/src/dev/index.css +3 -0
  58. package/src/dev/index.html +11 -0
  59. package/src/dev/index.js +5 -0
  60. package/src/dev/sampleValue1.json +4295 -0
  61. package/src/dev/sampleValue2.json +0 -0
  62. package/src/dev/sampleValueValid.json +411 -0
  63. package/src/dev/testComponents/TestStory.js +9 -0
  64. package/src/dev/testComponents/TestStory.stories.js +172 -0
  65. package/src/dev/testSampleValue.json +747 -0
  66. package/src/fromHTML.js +5 -0
  67. package/src/index.js +9 -0
  68. package/src/plugins/ListItem.js +49 -0
  69. package/src/plugins/SoftBreak.js +24 -0
  70. package/src/toHTML.js +7 -0
  71. package/src/toText.js +7 -0
  72. package/src/util.js +20 -0
  73. package/storyLoader.js +46 -0
  74. package/tailwind.config.js +5 -0
  75. package/webpack.config.build.js +53 -0
  76. package/webpack.config.dev.js +61 -0
  77. package/webpack.config.js +117 -0
@@ -0,0 +1,249 @@
1
+ import {
2
+ Button, Icon, Note,
3
+ } from "@contentful/forma-36-react-components";
4
+
5
+ export const SidebarEditorField = ({
6
+ field,
7
+ sdk,
8
+ storybookElement,
9
+ fieldKey,
10
+ value,
11
+ onChange,
12
+ }) => {
13
+ const deleteMVPEntry = (fieldKey, index) => {
14
+ let newMVPValue = JSON.parse(JSON.stringify(storybookElement.attributes[fieldKey]));
15
+
16
+ newMVPValue.splice(index, 1);
17
+
18
+ if (onChange) {
19
+ onChange(fieldKey, newMVPValue);
20
+ }
21
+ };
22
+
23
+ const getInputByField = (field, fieldKey) => {
24
+ switch (field?.control?.type) {
25
+ case "text":
26
+ return <input
27
+ type="text"
28
+ value={value}
29
+ onChange={e => onChange(fieldKey, e.target.value)} />;
30
+ case "boolean":
31
+ return (
32
+ <div>
33
+ <input
34
+ type="checkbox"
35
+ checked={value}
36
+ onChange={e => onChange(fieldKey, e.target.checked)} />
37
+ </div>
38
+ );
39
+ case "select":
40
+ return (
41
+ <select
42
+ value={value}
43
+ onChange={e => onChange(fieldKey, e.target.value)}>
44
+ {Object.keys(field.control.options).map(key => (
45
+ <option key={`select-option-${key}`} value={field.control.options[key]}>{key}</option>
46
+ ))}
47
+ </select>
48
+ );
49
+ case "number":
50
+ return (
51
+ <input
52
+ type="number"
53
+ value={value}
54
+ onChange={e => onChange(fieldKey, e.target.value)} />
55
+ );
56
+ case "range":
57
+ return (
58
+ <input
59
+ type="range"
60
+ min={field.control.min}
61
+ max={field.control.max}
62
+ step={field.control.step}
63
+ value={value}
64
+ onChange={e => onChange(fieldKey, e.target.value)} />
65
+ );
66
+ case "object":
67
+ return (
68
+ <textarea
69
+ value={value}
70
+ onChange={e => onChange(fieldKey, e.target.value)} />
71
+ );
72
+ case "radio":
73
+ return (
74
+ <div>
75
+ {Object.keys(field.control.options).map((key, index) => (
76
+ <div key={`radio-option-${key}`}>
77
+ <input id={`${fieldKey}-${index}`} type="radio" value={key} name={fieldKey} />
78
+ <label htmlFor={`${fieldKey}-${index}`}>{field.control.options[key]}</label>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ );
83
+ case "inline-radio":
84
+ return (
85
+ <div>
86
+ {Object.keys(field.control.options).map((key, index) => (
87
+ <div key={`inline-radio-option-${key}`} className="inline-check-wrapper">
88
+ <input id={`${fieldKey}-${index}`} type="radio" value={key} name={fieldKey} />
89
+ <label htmlFor={`${fieldKey}-${index}`}>{field.control.options[key]}</label>
90
+ </div>
91
+ ))}
92
+ </div>
93
+ );
94
+ case "check":
95
+ return (
96
+ <div>
97
+ {Object.keys(field.control.options).map((key, index) => (
98
+ <div key={`check-option-${key}`}>
99
+ <input id={`${fieldKey}-${index}`} type="checkbox" value={key} name={fieldKey} />
100
+ <label htmlFor={`${fieldKey}-${index}`}>{field.control.options[key]}</label>
101
+ </div>
102
+ ))}
103
+ </div>
104
+ );
105
+ case "inline-check":
106
+ return (
107
+ <div>
108
+ {Object.keys(field.control.options).map((key, index) => (
109
+ <div key={`inline-check-option-${key}`} className="inline-check-wrapper">
110
+ <input id={`${fieldKey}-${index}`} type="checkbox" value={key} name={fieldKey} />
111
+ <label htmlFor={`${fieldKey}-${index}`}>{field.control.options[key]}</label>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ );
116
+ case "color":
117
+ return (
118
+ <input
119
+ type="color"
120
+ value={value}
121
+ onChange={e => onChange(fieldKey, e.target.value)} />
122
+ );
123
+ case "date":
124
+ return (
125
+ <input
126
+ type="date"
127
+ value={value}
128
+ onChange={e => onChange(fieldKey, e.target.value)} />
129
+ );
130
+ case "multi-select":
131
+ return (
132
+ <select
133
+ multiple
134
+ value={value}
135
+ onChange={e => onChange(fieldKey, e.target.value)}>
136
+ {Object.keys(field.control.options).map(key => (
137
+ <option key={`select-option-${key}`} value={field.control.options[key]}>{key}</option>
138
+ ))}
139
+ </select>
140
+ );
141
+ case "contentful-content":
142
+ return (
143
+ <Button
144
+ onClick={() => {
145
+ sdk.dialogs.selectSingleEntry({contentTypes: field.control.contentTypes}).then(content => {
146
+ onChange(fieldKey, content);
147
+ });
148
+ }}
149
+ isFullWidth>Auswählen</Button>
150
+ );
151
+ case "contentful-contents":
152
+ return (
153
+ <Button
154
+ onClick={() => {
155
+ sdk.dialogs.selectMultipleEntries({contentTypes: field.control.contentTypes}).then(contents => {
156
+ onChange(fieldKey, contents);
157
+ });
158
+ }}
159
+ isFullWidth>Auswählen</Button>
160
+ );
161
+ case "cloudinary-image":
162
+ return (
163
+ <Button
164
+ onClick={() => {
165
+ sdk.dialogs.openCurrentApp({
166
+ width: "fullWidth",
167
+ title: "Select Image",
168
+ shouldCloseOnOverlayClick: true,
169
+ shouldCloseOnEscapePress: true,
170
+ parameters: {type: "cloudinary"},
171
+ }).then((image) => {
172
+ onChange(fieldKey, image);
173
+ });
174
+ }}
175
+ isFullWidth>Auswählen</Button>
176
+ );
177
+ case "cloudinary-images":
178
+ return (
179
+ <Button
180
+ onClick={() => {
181
+ sdk.dialogs.openCurrentApp({
182
+ width: "fullWidth",
183
+ title: "Select Images",
184
+ shouldCloseOnOverlayClick: true,
185
+ shouldCloseOnEscapePress: true,
186
+ parameters: {
187
+ type: "cloudinary",
188
+ multiple: true,
189
+ },
190
+ }).then((images) => {
191
+ onChange(fieldKey, images);
192
+ });
193
+ }}
194
+ isFullWidth>Auswählen</Button>
195
+ );
196
+ case "mvp":
197
+ return (
198
+ <>
199
+ {storybookElement?.attributes[fieldKey]?.map((f, index) => {
200
+ return (
201
+ <div className="mb-4" key={`mvp-${index}`}>
202
+ <div className="mb-2 flex items-center">
203
+ <b className="grow">{field.name} [{index.toString()}]</b>
204
+ <div className="cursor-pointer p-1" onClick={() => deleteMVPEntry(fieldKey, index)}><Icon icon="Delete" /></div>
205
+ </div>
206
+ {Object.keys(field.control.fields).map((key, mvpIndex) => {
207
+ let mvpField = field.control.fields[key];
208
+
209
+ value = storybookElement.attributes?.[fieldKey]?.[index]?.[key];
210
+
211
+ return (
212
+ <div key={`mvp-field-${key}`}>
213
+ <SidebarEditorField
214
+ field={mvpField}
215
+ fieldKey={fieldKey}
216
+ value={value}
217
+ storybookElement={storybookElement}
218
+ sdk={sdk}
219
+ onChange={(fK, value) => onChange(
220
+ fK, value, key, index,
221
+ )} />
222
+ </div>
223
+ );
224
+ })}
225
+ <hr className="my-4" style={{borderColor: "rgb(174, 193, 204)"}} />
226
+ </div>
227
+ );
228
+ })}
229
+ <Button onClick={() => onChange(fieldKey, [...storybookElement.attributes[fieldKey], {}])}>Neue Zeile</Button>
230
+ </>
231
+ );
232
+ default:
233
+ return <Note noteType="negative">Keine Konfiguration zu Feldtyp "{field?.control?.type}" gefunden</Note>;
234
+ }
235
+ };
236
+
237
+ const inputField = getInputByField(field, fieldKey);
238
+
239
+ if (!inputField) {
240
+ return null;
241
+ }
242
+
243
+ return (
244
+ <div key={`${field.name}`} className="mb-2">
245
+ <label>{field.name}</label>
246
+ {inputField}
247
+ </div>
248
+ );
249
+ };
@@ -0,0 +1,90 @@
1
+ * {
2
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
3
+ }
4
+
5
+ #sidebar-editor {
6
+ background-color: rgb(247, 249, 250);
7
+ height: 100%;
8
+ overflow-y: auto;
9
+ flex-basis: 400px;
10
+ flex-shrink: 0;
11
+ padding: 61px 16px 16px 16px;
12
+ }
13
+
14
+ select {
15
+ font-size: 0.875rem !important;
16
+ color: rgb(90, 101, 124) !important;
17
+ background-color: white !important;
18
+ border: 1px solid #cfd9e0 !important;
19
+ border-radius: 6px !important;
20
+ box-shadow: none !important;
21
+ filter: none !important;
22
+ }
23
+
24
+ p, label {
25
+ color: rgb(90, 101, 124);
26
+ }
27
+
28
+ label {
29
+ display: block;
30
+ margin: 0 0 4px 0 !important;
31
+ }
32
+
33
+ input[type="text"],
34
+ input[type="number"],
35
+ input[type="color"],
36
+ input[type="date"],
37
+ textarea {
38
+ display: block;
39
+ font-size: 0.875rem !important;
40
+ color: rgb(90, 101, 124) !important;
41
+ background-color: white !important;
42
+ border: 1px solid #cfd9e0 !important;
43
+ border-radius: 6px !important;
44
+ box-shadow: none !important;
45
+ filter: none !important;
46
+ width: 100% !important;
47
+ padding: 8px 0.75rem;
48
+ }
49
+
50
+ input[type="color"] {
51
+ padding: 0;
52
+ }
53
+
54
+ input[type="range"] {
55
+ display: block;
56
+ width: 100%;
57
+ }
58
+
59
+ input[type="radio"],
60
+ input[type="checkbox"] {
61
+ margin-right: 4px;
62
+ }
63
+
64
+ /* button[data-test-id="cf-ui-button"] span {
65
+ color: #036fe3 !important;
66
+ }
67
+
68
+ button[data-test-id="cf-ui-button"] svg {
69
+ fill: #036fe3 !important;
70
+ }
71
+
72
+ button[data-test-id="cf-ui-button"]:hover span {
73
+ color: #FFFFFF !important;
74
+ }
75
+
76
+ button[data-test-id="cf-ui-button"]:hover svg {
77
+ fill: #FFFFFF !important;
78
+ } */
79
+
80
+ .inline-check-wrapper {
81
+ display: inline-block;
82
+ margin-right: 8px;
83
+ }
84
+
85
+ button {
86
+ width: 100%;
87
+ padding: 8px !important;
88
+ color: #036fe3 !important;
89
+ border: 1px solid #036fe3 !important;
90
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ Accordion,
3
+ AccordionItem,
4
+ Button,
5
+ FormLabel,
6
+ Note,
7
+ Switch,
8
+ Heading,
9
+ IconButton,
10
+ Icon,
11
+ } from "@contentful/forma-36-react-components";
12
+
13
+ import "./SidebarEditor.css";
14
+ import {SidebarEditorField} from "./SidebarEditor/SidebarEditorField";
15
+ import {ToolMargin} from "./Tools/Margin";
16
+
17
+ const SidebarEditor = ({
18
+ sdk,
19
+ storybookElement,
20
+ storybookStories,
21
+ onClose,
22
+ onChange,
23
+ onDelete,
24
+ onMove,
25
+ }) => {
26
+ const fields = {
27
+ fields: {},
28
+ tables: {},
29
+ };
30
+
31
+ const story = storybookStories.find(v => storybookElement.block === v.id);
32
+
33
+ if (story) {
34
+ Object.keys(story.argTypes).forEach(key => {
35
+ const argType = story.argTypes[key];
36
+
37
+ if (argType.table?.category) {
38
+ if (fields.tables[argType.table.category]) {
39
+ fields.tables[argType.table.category][key] = argType;
40
+ } else {
41
+ fields.tables[argType.table.category] = {[key]: argType};
42
+ }
43
+ } else {
44
+ fields.fields[key] = argType;
45
+ }
46
+ });
47
+ }
48
+
49
+ const handleFieldValueChange = (
50
+ fieldKey, value, mvpFieldKey, mvpIndex,
51
+ ) => {
52
+ if (onChange) {
53
+ if (mvpFieldKey !== undefined && mvpIndex !== undefined) {
54
+ let newMVPValue = storybookElement.attributes[fieldKey].map((f, index) => {
55
+ if (index === mvpIndex) {
56
+ return {
57
+ ...f,
58
+ [mvpFieldKey]: value,
59
+ };
60
+ }
61
+
62
+ return f;
63
+ });
64
+
65
+ onChange({
66
+ ...storybookElement,
67
+ attributes: {
68
+ ...storybookElement.attributes,
69
+ [fieldKey]: newMVPValue,
70
+ },
71
+ });
72
+ } else {
73
+ onChange({
74
+ ...storybookElement,
75
+ attributes: {
76
+ ...storybookElement.attributes,
77
+ [fieldKey]: value,
78
+ },
79
+ });
80
+ }
81
+ }
82
+ };
83
+
84
+ return (
85
+ <div id="sidebar-editor">
86
+ <BlockSelect stories={storybookStories} active={storybookElement} onChange={onChange}/>
87
+ {storybookElement?.block && (
88
+ <div>
89
+ <div>
90
+ <div className="flex">
91
+ <div className="grow">
92
+ <Heading>{storybookElement.block}</Heading>
93
+ </div>
94
+ {!!onClose && <div className="cursor-pointer p-1" onClick={onClose}><Icon icon="Close" /></div>}
95
+ </div>
96
+ <VariantSelect story={story} onChange={onChange}/>
97
+ </div>
98
+ <div className="flex items-center">
99
+ <select
100
+ value={storybookElement.attributes.blockWidth}
101
+ onChange={e => handleFieldValueChange("blockWidth", e.target.value)}
102
+ className="grow">
103
+ <option disabled>Breite wählen</option>
104
+ <option value={undefined}>Volle Breite</option>
105
+ <option value={"article"}>Artikel Breite</option>
106
+ <option value={"site"}>Seiten Breite</option>
107
+ <option value={"small"}>Klein</option>
108
+ </select>
109
+ <div className="cursor-pointer p-2" title="Nach oben verschieben" onClick={() => onMove && onMove(storybookElement, "up")}>
110
+
111
+ </div>
112
+ <div className="cursor-pointer p-2" title="Nach unten verschieben" onClick={() => onMove && onMove(storybookElement, "down")}>
113
+
114
+ </div>
115
+ <div className="p-2 pt-3">
116
+ <ToolMargin
117
+ margin={storybookElement.attributes.margin}
118
+ onChange={value => handleFieldValueChange("margin", value)} />
119
+ </div>
120
+ <div className="cursor-pointer p-2" title="Löschen" onClick={() => onDelete && onDelete(storybookElement)}>
121
+ 🗑
122
+ </div>
123
+ </div>
124
+ <hr className="my-4" style={{borderColor: "rgb(174, 193, 204)"}}/>
125
+ </div>
126
+ )}
127
+ {!!story && (
128
+ <>
129
+ {Object.keys(fields.fields).map(key => {
130
+ const field = fields.fields[key];
131
+
132
+ return <SidebarEditorField
133
+ sdk={sdk}
134
+ value={storybookElement?.attributes?.[key]}
135
+ key={key}
136
+ storybookElement={storybookElement}
137
+ fieldKey={key}
138
+ field={field}
139
+ onChange={(
140
+ key, value, mvpField, mvpIndex,
141
+ ) => handleFieldValueChange(
142
+ key, value, mvpField, mvpIndex,
143
+ )}
144
+ />;
145
+ })}
146
+ <Accordion>
147
+ {Object.keys(fields.tables).map(tableKey => {
148
+ return (
149
+ <AccordionItem key={`accordion-item-${tableKey}`} title={tableKey}>
150
+ {Object.keys(fields.tables[tableKey]).map(key => {
151
+ const field = fields.tables[tableKey][key];
152
+
153
+ return <SidebarEditorField
154
+ sdk={sdk}
155
+ value={storybookElement?.attributes?.[key]}
156
+ key={key}
157
+ fieldKey={key}
158
+ story={storybookElement}
159
+ field={field}
160
+ onChange={value => handleFieldValueChange(key, value)}
161
+ />;
162
+ })}
163
+ </AccordionItem>
164
+ );
165
+ })}
166
+ </Accordion>
167
+ </>
168
+ )}
169
+ </div>
170
+ );
171
+ };
172
+
173
+ const VariantSelect = ({
174
+ story,
175
+ onChange,
176
+ }) => {
177
+ const stories = story?.stories?.filter(s => s.title !== story.title) || [];
178
+
179
+ if (stories.length <= 1 || !onChange) {
180
+ return null;
181
+ }
182
+
183
+ return <select
184
+ className="mt-2"
185
+ onChange={e => onChange && onChange({
186
+ block: story.id,
187
+ attributes: {...stories.find(s => s.title === e.target.value)?.args || {}},
188
+ })}>
189
+ <option>Preset wählen</option>
190
+ {stories.map(s => (
191
+ <option key={`variant-option-${s.title}`} value={s.title}>{s.title}</option>
192
+ ))}
193
+ </select>;
194
+ };
195
+
196
+ const BlockSelect = ({
197
+ stories,
198
+ active,
199
+ onChange,
200
+ }) => {
201
+ if (!onChange) {
202
+ return null;
203
+ }
204
+
205
+ const onSelectChange = (story) => {
206
+ if (!story) {
207
+ return onChange({}); // reset to empty
208
+ }
209
+
210
+ // reset to the first story
211
+ onChange({
212
+ block: story.id,
213
+ attributes: {...(story?.stories?.[0]?.args || {})},
214
+ });
215
+ };
216
+
217
+ return (
218
+ <select
219
+ className="mt-2"
220
+ onChange={e => onSelectChange(stories.find(s => s.id === e.target.value))}
221
+ >
222
+ <option>Element wählen</option>
223
+ {stories.map(s => (
224
+ <option
225
+ key={`variant-option-${s.id}`}
226
+ selected={active?.block === s.id}
227
+ value={s.id}
228
+ >
229
+ {s.title}
230
+ </option>
231
+ ))}
232
+ </select>
233
+ );
234
+ };
235
+
236
+ export default SidebarEditor;