@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-clms-theme",
3
- "version": "1.0.186",
3
+ "version": "1.0.187",
4
4
  "description": "volto-clms-theme: Volto theme for CLMS site",
5
5
  "main": "src/index.js",
6
6
  "author": "CodeSyntax for the European Environment Agency",
@@ -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;