@eeacms/volto-clms-theme 1.0.186 → 1.0.187
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx +378 -0
- /package/src/customizations/{volto/packages → @plone}/volto-slate/editor/less/globals.less +0 -0
- /package/src/customizations/{volto/packages → @plone}/volto-slate/editor/ui/Toolbar.jsx +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
### [1.0.187](https://github.com/eea/volto-clms-theme/compare/1.0.186...1.0.187) - 15 March 2023
|
|
8
|
+
|
|
9
|
+
#### :hammer_and_wrench: Others
|
|
10
|
+
|
|
11
|
+
- disable no-direct-mutatuion [ionlizarazu - [`dd11dfd`](https://github.com/eea/volto-clms-theme/commit/dd11dfd1cce62ff29771809f27c01c30aad10e9f)]
|
|
12
|
+
- add an empty slate text editor if there are no children in the slate editor [ionlizarazu - [`fb77de2`](https://github.com/eea/volto-clms-theme/commit/fb77de261fd0d40fae42fa6301ea9b485e144320)]
|
|
13
|
+
- mv files to override and initial original copy of SlateEditor.jsx [ionlizarazu - [`cebd0fc`](https://github.com/eea/volto-clms-theme/commit/cebd0fc3ed517cc0b586a5e3060da77ce94763d1)]
|
|
7
14
|
### [1.0.186](https://github.com/eea/volto-clms-theme/compare/1.0.185...1.0.186) - 14 March 2023
|
|
8
15
|
|
|
9
16
|
#### :bug: Bug Fixes
|
package/package.json
CHANGED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { isEqual } from 'lodash';
|
|
4
|
+
import { Transforms, Editor } from 'slate'; // , Transforms
|
|
5
|
+
import { Slate, Editable, ReactEditor } from 'slate-react';
|
|
6
|
+
import React, { Component } from 'react'; // , useState
|
|
7
|
+
import { v4 as uuid } from 'uuid';
|
|
8
|
+
|
|
9
|
+
import config from '@plone/volto/registry';
|
|
10
|
+
|
|
11
|
+
import { Element, Leaf } from '@plone/volto-slate/editor/render';
|
|
12
|
+
|
|
13
|
+
import withTestingFeatures from '@plone/volto-slate/editor/extensions/withTestingFeatures';
|
|
14
|
+
import {
|
|
15
|
+
makeEditor,
|
|
16
|
+
toggleInlineFormat,
|
|
17
|
+
toggleMark,
|
|
18
|
+
parseDefaultSelection,
|
|
19
|
+
} from '@plone/volto-slate/utils';
|
|
20
|
+
import { InlineToolbar } from '@plone/volto-slate/editor/ui';
|
|
21
|
+
import EditorContext from '@plone/volto-slate/editor/EditorContext';
|
|
22
|
+
|
|
23
|
+
import isHotkey from 'is-hotkey';
|
|
24
|
+
|
|
25
|
+
import '@plone/volto-slate/editor/less/editor.less';
|
|
26
|
+
|
|
27
|
+
import Toolbar from './ui/Toolbar';
|
|
28
|
+
|
|
29
|
+
const handleHotKeys = (editor, event, config) => {
|
|
30
|
+
let wasHotkey = false;
|
|
31
|
+
|
|
32
|
+
for (const hk of Object.entries(config.hotkeys)) {
|
|
33
|
+
const [shortcut, { format, type }] = hk;
|
|
34
|
+
if (isHotkey(shortcut, event)) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
|
|
37
|
+
if (type === 'inline') {
|
|
38
|
+
toggleInlineFormat(editor, format);
|
|
39
|
+
} else {
|
|
40
|
+
// type === 'mark'
|
|
41
|
+
toggleMark(editor, format);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
wasHotkey = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return wasHotkey;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// TODO: implement onFocus
|
|
52
|
+
class SlateEditor extends Component {
|
|
53
|
+
constructor(props) {
|
|
54
|
+
super(props);
|
|
55
|
+
|
|
56
|
+
this.createEditor = this.createEditor.bind(this);
|
|
57
|
+
this.multiDecorator = this.multiDecorator.bind(this);
|
|
58
|
+
this.handleChange = this.handleChange.bind(this);
|
|
59
|
+
this.getSavedSelection = this.getSavedSelection.bind(this);
|
|
60
|
+
this.setSavedSelection = this.setSavedSelection.bind(this);
|
|
61
|
+
|
|
62
|
+
this.savedSelection = null;
|
|
63
|
+
|
|
64
|
+
const uid = uuid(); // used to namespace the editor's plugins
|
|
65
|
+
|
|
66
|
+
this.slateSettings = props.slateSettings || config.settings.slate;
|
|
67
|
+
|
|
68
|
+
this.state = {
|
|
69
|
+
editor: this.createEditor(uid),
|
|
70
|
+
showExpandedToolbar: config.settings.slate.showExpandedToolbar,
|
|
71
|
+
internalValue: this.props.value || this.slateSettings.defaultValue(),
|
|
72
|
+
uid,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.editor = null;
|
|
76
|
+
this.selectionTimeout = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getSavedSelection() {
|
|
80
|
+
return this.savedSelection;
|
|
81
|
+
}
|
|
82
|
+
setSavedSelection(selection) {
|
|
83
|
+
this.savedSelection = selection;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
createEditor(uid) {
|
|
87
|
+
// extensions are "editor plugins" or "editor wrappers". It's a similar
|
|
88
|
+
// similar to OOP inheritance, where a callable creates a new copy of the
|
|
89
|
+
// editor, while replacing or adding new capabilities to that editor.
|
|
90
|
+
// Extensions are purely JS, no React components.
|
|
91
|
+
const editor = makeEditor({ extensions: this.props.extensions });
|
|
92
|
+
|
|
93
|
+
// When the editor loses focus it no longer has a valid selections. This
|
|
94
|
+
// makes it impossible to have complex types of interactions (like filling
|
|
95
|
+
// in another text box, operating a select menu, etc). For this reason we
|
|
96
|
+
// save the active selection
|
|
97
|
+
|
|
98
|
+
editor.getSavedSelection = this.getSavedSelection;
|
|
99
|
+
editor.setSavedSelection = this.setSavedSelection;
|
|
100
|
+
editor.uid = uid || this.state.uid;
|
|
101
|
+
|
|
102
|
+
return editor;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
handleChange(value) {
|
|
106
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
107
|
+
this.setState({ internalValue: value });
|
|
108
|
+
if (this.props.onChange && !isEqual(value, this.props.value)) {
|
|
109
|
+
this.props.onChange(value, this.editor);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
multiDecorator([node, path]) {
|
|
115
|
+
// Decorations (such as higlighting node types, selection, etc).
|
|
116
|
+
const { runtimeDecorators = [] } = this.slateSettings;
|
|
117
|
+
return runtimeDecorators.reduce(
|
|
118
|
+
(acc, deco) => deco(this.state.editor, [node, path], acc),
|
|
119
|
+
[],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
componentDidMount() {
|
|
124
|
+
// watch the dom change
|
|
125
|
+
|
|
126
|
+
if (this.props.selected) {
|
|
127
|
+
let focused = true;
|
|
128
|
+
try {
|
|
129
|
+
focused = ReactEditor.isFocused(this.state.editor);
|
|
130
|
+
} catch {}
|
|
131
|
+
if (!focused) {
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
try {
|
|
134
|
+
ReactEditor.focus(this.state.editor);
|
|
135
|
+
} catch {}
|
|
136
|
+
}, 100); // flush
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
componentWillUnmount() {
|
|
142
|
+
this.isUnmounted = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
componentDidUpdate(prevProps) {
|
|
146
|
+
if (!isEqual(prevProps.extensions, this.props.extensions)) {
|
|
147
|
+
this.setState({ editor: this.createEditor() });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
this.props.value &&
|
|
153
|
+
!isEqual(this.props.value, this.state.internalValue)
|
|
154
|
+
) {
|
|
155
|
+
const { editor } = this.state;
|
|
156
|
+
editor.children = this.props.value;
|
|
157
|
+
|
|
158
|
+
if (this.props.defaultSelection) {
|
|
159
|
+
const selection = parseDefaultSelection(
|
|
160
|
+
editor,
|
|
161
|
+
this.props.defaultSelection,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
ReactEditor.focus(editor);
|
|
165
|
+
Transforms.select(editor, selection);
|
|
166
|
+
} else {
|
|
167
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.setState({
|
|
171
|
+
internalValue: this.props.value,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { editor } = this.state;
|
|
177
|
+
|
|
178
|
+
if (!prevProps.selected && this.props.selected) {
|
|
179
|
+
// if the SlateEditor becomes selected from unselected
|
|
180
|
+
if (window.getSelection().type === 'None') {
|
|
181
|
+
// TODO: why is this condition checked?
|
|
182
|
+
if (this.state.editor.children.length === 0) {
|
|
183
|
+
// eslint-disable-next-line react/no-direct-mutation-state
|
|
184
|
+
this.state.editor.children = [
|
|
185
|
+
{ children: [{ text: '' }], type: 'p' },
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
Transforms.select(
|
|
189
|
+
this.state.editor,
|
|
190
|
+
Editor.range(this.state.editor, Editor.start(this.state.editor, [])),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ReactEditor.focus(this.state.editor);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.props.selected && this.props.onUpdate) {
|
|
198
|
+
this.props.onUpdate(editor);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
shouldComponentUpdate(nextProps, nextState) {
|
|
203
|
+
const { selected = true, value, readOnly } = nextProps;
|
|
204
|
+
const res =
|
|
205
|
+
selected ||
|
|
206
|
+
this.props.selected !== selected ||
|
|
207
|
+
this.props.readOnly !== readOnly ||
|
|
208
|
+
!isEqual(value, this.props.value);
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
render() {
|
|
213
|
+
const {
|
|
214
|
+
selected,
|
|
215
|
+
placeholder,
|
|
216
|
+
onKeyDown,
|
|
217
|
+
testingEditorRef,
|
|
218
|
+
readOnly,
|
|
219
|
+
className,
|
|
220
|
+
renderExtensions = [],
|
|
221
|
+
editableProps = {},
|
|
222
|
+
} = this.props;
|
|
223
|
+
const slateSettings = this.slateSettings;
|
|
224
|
+
|
|
225
|
+
// renderExtensions is needed because the editor is memoized, so if these
|
|
226
|
+
// extensions need an updated state (for example to insert updated
|
|
227
|
+
// blockProps) then we need to always wrap the editor with them
|
|
228
|
+
const editor = renderExtensions.reduce(
|
|
229
|
+
(acc, apply) => apply(acc),
|
|
230
|
+
this.state.editor,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Reset selection if field is reset
|
|
234
|
+
if (
|
|
235
|
+
editor.selection &&
|
|
236
|
+
this.props.value?.length === 1 &&
|
|
237
|
+
this.props.value[0].children.length === 1 &&
|
|
238
|
+
this.props.value[0].children[0].text === ''
|
|
239
|
+
) {
|
|
240
|
+
Transforms.select(editor, {
|
|
241
|
+
anchor: { path: [0, 0], offset: 0 },
|
|
242
|
+
focus: { path: [0, 0], offset: 0 },
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
this.editor = editor;
|
|
246
|
+
|
|
247
|
+
if (testingEditorRef) {
|
|
248
|
+
testingEditorRef.current = editor;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// debug-values are `data-` HTML attributes in withTestingFeatures HOC
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div
|
|
255
|
+
{...this.props['debug-values']}
|
|
256
|
+
className={cx('slate-editor', {
|
|
257
|
+
'show-toolbar': this.state.showExpandedToolbar,
|
|
258
|
+
selected,
|
|
259
|
+
})}
|
|
260
|
+
tabIndex={-1}
|
|
261
|
+
>
|
|
262
|
+
<EditorContext.Provider value={editor}>
|
|
263
|
+
<Slate
|
|
264
|
+
editor={editor}
|
|
265
|
+
value={this.props.value || slateSettings.defaultValue()}
|
|
266
|
+
onChange={this.handleChange}
|
|
267
|
+
>
|
|
268
|
+
{selected ? (
|
|
269
|
+
<>
|
|
270
|
+
<InlineToolbar
|
|
271
|
+
editor={editor}
|
|
272
|
+
className={className}
|
|
273
|
+
slateSettings={this.props.slateSettings}
|
|
274
|
+
/>
|
|
275
|
+
{Object.keys(slateSettings.elementToolbarButtons).map(
|
|
276
|
+
(t, i) => {
|
|
277
|
+
return (
|
|
278
|
+
<Toolbar elementType={t} key={i}>
|
|
279
|
+
{slateSettings.elementToolbarButtons[t].map(
|
|
280
|
+
(Btn, b) => {
|
|
281
|
+
return <Btn editor={editor} key={b} />;
|
|
282
|
+
},
|
|
283
|
+
)}
|
|
284
|
+
</Toolbar>
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
)}
|
|
288
|
+
</>
|
|
289
|
+
) : (
|
|
290
|
+
''
|
|
291
|
+
)}
|
|
292
|
+
<Editable
|
|
293
|
+
tabIndex={this.props.tabIndex || 0}
|
|
294
|
+
readOnly={readOnly}
|
|
295
|
+
placeholder={placeholder}
|
|
296
|
+
renderElement={(props) => <Element {...props} />}
|
|
297
|
+
renderLeaf={(props) => <Leaf {...props} />}
|
|
298
|
+
decorate={this.multiDecorator}
|
|
299
|
+
spellCheck={false}
|
|
300
|
+
scrollSelectionIntoView={
|
|
301
|
+
slateSettings.scrollIntoView ? undefined : () => null
|
|
302
|
+
}
|
|
303
|
+
onBlur={() => {
|
|
304
|
+
this.props.onBlur && this.props.onBlur();
|
|
305
|
+
return null;
|
|
306
|
+
}}
|
|
307
|
+
onClick={this.props.onClick}
|
|
308
|
+
onSelect={(e) => {
|
|
309
|
+
if (!selected && this.props.onFocus) {
|
|
310
|
+
// we can't overwrite the onFocus of Editable, as the onFocus
|
|
311
|
+
// in Slate has too much builtin behaviour that's not
|
|
312
|
+
// accessible otherwise. Instead we try to detect such an
|
|
313
|
+
// event based on observing selected state
|
|
314
|
+
if (!editor.selection) {
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
this.props.onFocus();
|
|
317
|
+
}, 100); // TODO: why 100 is chosen here?
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.selectionTimeout) clearTimeout(this.selectionTimeout);
|
|
322
|
+
this.selectionTimeout = setTimeout(() => {
|
|
323
|
+
if (
|
|
324
|
+
editor.selection &&
|
|
325
|
+
!isEqual(editor.selection, this.savedSelection) &&
|
|
326
|
+
!this.isUnmounted
|
|
327
|
+
) {
|
|
328
|
+
this.setState((state) => ({ update: !this.state.update }));
|
|
329
|
+
this.setSavedSelection(
|
|
330
|
+
JSON.parse(JSON.stringify(editor.selection)),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}, 200);
|
|
334
|
+
}}
|
|
335
|
+
onKeyDown={(event) => {
|
|
336
|
+
const handled = handleHotKeys(editor, event, slateSettings);
|
|
337
|
+
if (handled) return;
|
|
338
|
+
onKeyDown && onKeyDown({ editor, event });
|
|
339
|
+
}}
|
|
340
|
+
{...editableProps}
|
|
341
|
+
/>
|
|
342
|
+
{selected &&
|
|
343
|
+
slateSettings.persistentHelpers.map((Helper, i) => {
|
|
344
|
+
return <Helper key={i} editor={editor} />;
|
|
345
|
+
})}
|
|
346
|
+
{this.props.debug ? (
|
|
347
|
+
<ul>
|
|
348
|
+
<li>{selected ? 'selected' : 'no-selected'}</li>
|
|
349
|
+
<li>
|
|
350
|
+
savedSelection: {JSON.stringify(editor.getSavedSelection())}
|
|
351
|
+
</li>
|
|
352
|
+
<li>live selection: {JSON.stringify(editor.selection)}</li>
|
|
353
|
+
<li>children: {JSON.stringify(editor.children)}</li>
|
|
354
|
+
<li> {selected ? 'selected' : 'notselected'}</li>
|
|
355
|
+
<li>
|
|
356
|
+
{ReactEditor.isFocused(editor) ? 'focused' : 'unfocused'}
|
|
357
|
+
</li>
|
|
358
|
+
</ul>
|
|
359
|
+
) : (
|
|
360
|
+
''
|
|
361
|
+
)}
|
|
362
|
+
{this.props.children}
|
|
363
|
+
</Slate>
|
|
364
|
+
</EditorContext.Provider>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
SlateEditor.defaultProps = {
|
|
371
|
+
extensions: [],
|
|
372
|
+
className: '',
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// May be needed to wrap in React.memo(), it used to be wrapped in connect()
|
|
376
|
+
export default __CLIENT__ && window?.Cypress
|
|
377
|
+
? withTestingFeatures(SlateEditor)
|
|
378
|
+
: SlateEditor;
|
|
File without changes
|
|
File without changes
|