@eeacms/volto-eea-website-theme 1.27.2 → 1.28.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.
package/.eslintrc.js CHANGED
@@ -1,40 +1,43 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
-
4
- const projectRootPath = fs.existsSync('./project')
5
- ? fs.realpathSync('./project')
6
- : fs.realpathSync(__dirname + '/../../../');
7
- const packageJson = require(path.join(projectRootPath, 'package.json'));
8
- const jsConfig = require(path.join(projectRootPath, 'jsconfig.json')).compilerOptions;
9
-
10
- const pathsConfig = jsConfig.paths;
3
+ const projectRootPath = fs.realpathSync(__dirname + '/../../../');
11
4
 
12
5
  let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto');
6
+ let configFile;
7
+ if (fs.existsSync(`${projectRootPath}/tsconfig.json`))
8
+ configFile = `${projectRootPath}/tsconfig.json`;
9
+ else if (fs.existsSync(`${projectRootPath}/jsconfig.json`))
10
+ configFile = `${projectRootPath}/jsconfig.json`;
11
+
12
+ if (configFile) {
13
+ const jsConfig = require(configFile).compilerOptions;
14
+ const pathsConfig = jsConfig.paths;
15
+ if (pathsConfig['@plone/volto'])
16
+ voltoPath = `./${jsConfig.baseUrl}/${pathsConfig['@plone/volto'][0]}`;
17
+ }
13
18
 
14
- Object.keys(pathsConfig).forEach(pkg => {
15
- if (pkg === '@plone/volto') {
16
- voltoPath = `./${jsConfig.baseUrl}/${pathsConfig[pkg][0]}`;
17
- }
18
- });
19
19
  const AddonConfigurationRegistry = require(`${voltoPath}/addon-registry.js`);
20
20
  const reg = new AddonConfigurationRegistry(projectRootPath);
21
21
 
22
22
  // Extends ESlint configuration for adding the aliases to `src` directories in Volto addons
23
- const addonAliases = Object.keys(reg.packages).map(o => [
23
+ const addonAliases = Object.keys(reg.packages).map((o) => [
24
24
  o,
25
25
  reg.packages[o].modulePath,
26
26
  ]);
27
27
 
28
+ const addonExtenders = reg.getEslintExtenders().map((m) => require(m));
28
29
 
29
- module.exports = {
30
- extends: `${projectRootPath}/node_modules/@plone/volto/.eslintrc`,
30
+ const defaultConfig = {
31
+ extends: `${voltoPath}/.eslintrc`,
31
32
  settings: {
32
33
  'import/resolver': {
33
34
  alias: {
34
35
  map: [
35
36
  ['@plone/volto', '@plone/volto/src'],
37
+ ['@plone/volto-slate', '@plone/volto/packages/volto-slate/src'],
36
38
  ...addonAliases,
37
39
  ['@package', `${__dirname}/src`],
40
+ ['@root', `${__dirname}/src`],
38
41
  ['~', `${__dirname}/src`],
39
42
  ],
40
43
  extensions: ['.js', '.jsx', '.json'],
@@ -51,6 +54,12 @@ module.exports = {
51
54
  allowReferrer: true,
52
55
  },
53
56
  ],
54
- }
57
+ },
55
58
  };
56
59
 
60
+ const config = addonExtenders.reduce(
61
+ (acc, extender) => extender.modify(acc),
62
+ defaultConfig,
63
+ );
64
+
65
+ module.exports = config;
package/CHANGELOG.md CHANGED
@@ -4,16 +4,38 @@ 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.27.2](https://github.com/eea/volto-eea-website-theme/compare/1.27.1...1.27.2) - 23 January 2024
7
+ ### [1.28.1](https://github.com/eea/volto-eea-website-theme/compare/1.28.0...1.28.1) - 19 February 2024
8
8
 
9
9
  #### :bug: Bug Fixes
10
10
 
11
+ - fix: Don't crash when rendering invalid slate [Tiberiu Ichim - [`9b7bc96`](https://github.com/eea/volto-eea-website-theme/commit/9b7bc9622d55f0912f13252ef80255f9cbb778d5)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - update readme [Razvan - [`ba82be2`](https://github.com/eea/volto-eea-website-theme/commit/ba82be250e82120958d18137c5972150db977b6b)]
16
+ ### [1.28.0](https://github.com/eea/volto-eea-website-theme/compare/1.27.2...1.28.0) - 19 February 2024
17
+
18
+ #### :bug: Bug Fixes
19
+
20
+ - fix(toc): make toc work, refs #265201 [Razvan - [`507adc2`](https://github.com/eea/volto-eea-website-theme/commit/507adc29f0a2e144933b06dfcf0856f1ac7efc98)]
21
+ - fix: volto slate when used in metadata block with SlateJSONField - refs #264239 [Miu Razvan - [`51682c4`](https://github.com/eea/volto-eea-website-theme/commit/51682c42001f6aa3433feff62c5f8536283de990)]
11
22
  - fix(lint): service so that it work with editor and command line tool [David Ichim - [`ad43bc2`](https://github.com/eea/volto-eea-website-theme/commit/ad43bc2f9bfc3e272d30b35db9d4b160a8edcbec)]
12
23
 
24
+ #### :house: Internal changes
25
+
26
+ - chore: package.json [Alin Voinea - [`08beb70`](https://github.com/eea/volto-eea-website-theme/commit/08beb706d9021a89c80acc5aa7c94350195f7de7)]
27
+
13
28
  #### :hammer_and_wrench: Others
14
29
 
30
+ - bump version [Razvan - [`721e939`](https://github.com/eea/volto-eea-website-theme/commit/721e939d12e324b459ebfa78a2e656ee7142a3d6)]
31
+ - merge master into this branch [Razvan - [`586c8f9`](https://github.com/eea/volto-eea-website-theme/commit/586c8f910bac55a043bd8dda60e9444bd2ae1663)]
32
+ - Add Sonarqube tag using freshwater-frontend addons list [EEA Jenkins - [`fd90044`](https://github.com/eea/volto-eea-website-theme/commit/fd9004442a9d1d465f7601ecdefe3e23c61e6a9c)]
33
+ - Add Sonarqube tag using insitu-frontend addons list [EEA Jenkins - [`4bc3dd3`](https://github.com/eea/volto-eea-website-theme/commit/4bc3dd3ae412a66befd04b5b80fab3716c929240)]
34
+ - test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c4dbd28`](https://github.com/eea/volto-eea-website-theme/commit/c4dbd289358205bc2d849aab7edb11ccf3b89cee)]
15
35
  - fix tests [Razvan - [`042330b`](https://github.com/eea/volto-eea-website-theme/commit/042330bc97d32ffe7ba769b4f2453f71cffed706)]
16
36
  - remove RemoveSchema logic [Razvan - [`08d10f8`](https://github.com/eea/volto-eea-website-theme/commit/08d10f8bf6f75478260e4e4c66d7316ba87b907a)]
37
+ ### [1.27.2](https://github.com/eea/volto-eea-website-theme/compare/1.27.1...1.27.2) - 24 January 2024
38
+
17
39
  ### [1.27.1](https://github.com/eea/volto-eea-website-theme/compare/1.27.0...1.27.1) - 18 January 2024
18
40
 
19
41
  #### :bug: Bug Fixes
package/README.md CHANGED
@@ -27,6 +27,10 @@ See [Storybook](https://eea.github.io/eea-storybook/).
27
27
 
28
28
  ## Volto customizations
29
29
 
30
+ - `volto-slate/editor/SlateEditor` -> When two slates looks at the same prop changing one slate and updating the other should be handled properly. This change makes replacing the old value of slate work in sync with the other slates that watches the same prop [ref](https://taskman.eionet.europa.eu/issues/264239#note-11).
31
+
32
+ **!!IMPORTANT**: This change requires volto@^16.26.1
33
+
30
34
  - `volto/components/manage/Sidebar/SidebarPopup` -> https://github.com/plone/volto/pull/5520
31
35
 
32
36
  ## Getting started
@@ -1,3 +1,5 @@
1
+ require('dotenv').config({ path: __dirname + '/.env' })
2
+
1
3
  module.exports = {
2
4
  testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'],
3
5
  collectCoverageFrom: [
@@ -9,31 +11,38 @@ module.exports = {
9
11
  '@plone/volto/cypress': '<rootDir>/node_modules/@plone/volto/cypress',
10
12
  '@plone/volto/babel': '<rootDir>/node_modules/@plone/volto/babel',
11
13
  '@plone/volto/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
12
- '@package/(.*)$': '<rootDir>/src/$1',
13
- '@root/(.*)$': '<rootDir>/src/$1',
14
+ '@package/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
15
+ '@root/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
14
16
  '@plone/volto-quanta/(.*)$': '<rootDir>/src/addons/volto-quanta/src/$1',
15
17
  '@eeacms/(.*?)/(.*)$': '<rootDir>/node_modules/@eeacms/$1/src/$2',
16
- '@plone/volto-slate':
18
+ '@plone/volto-slate$':
17
19
  '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src',
20
+ '@plone/volto-slate/(.*)$':
21
+ '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src/$1',
18
22
  '~/(.*)$': '<rootDir>/src/$1',
19
23
  'load-volto-addons':
20
24
  '<rootDir>/node_modules/@plone/volto/jest-addons-loader.js',
21
25
  },
26
+ transformIgnorePatterns: [
27
+ '/node_modules/(?!(@plone|@root|@package|@eeacms)/).*/',
28
+ ],
22
29
  transform: {
23
30
  '^.+\\.js(x)?$': 'babel-jest',
24
31
  '^.+\\.(png)$': 'jest-file',
25
32
  '^.+\\.(jpg)$': 'jest-file',
26
33
  '^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js',
27
34
  },
28
- transformIgnorePatterns: [
29
- 'node_modules/(?!@eeacms)/volto-eea-design-system/ui',
30
- ],
31
35
  coverageThreshold: {
32
36
  global: {
33
- branches: 0,
34
- functions: 0,
35
- lines: 0,
36
- statements: 0,
37
+ branches: 5,
38
+ functions: 5,
39
+ lines: 5,
40
+ statements: 5,
37
41
  },
38
42
  },
39
- };
43
+ ...(process.env.JEST_USE_SETUP === 'ON' && {
44
+ setupFilesAfterEnv: [
45
+ '<rootDir>/node_modules/@eeacms/volto-eea-website-theme/jest.setup.js',
46
+ ],
47
+ }),
48
+ }
package/jest.setup.js ADDED
@@ -0,0 +1,65 @@
1
+ import { jest } from '@jest/globals';
2
+ import configureStore from 'redux-mock-store';
3
+ import thunk from 'redux-thunk';
4
+ import { blocksConfig } from '@plone/volto/config/Blocks';
5
+ import installSlate from '@plone/volto-slate/index';
6
+
7
+ var mockSemanticComponents = jest.requireActual('semantic-ui-react');
8
+ var mockComponents = jest.requireActual('@plone/volto/components');
9
+ var config = jest.requireActual('@plone/volto/registry').default;
10
+
11
+ config.blocks.blocksConfig = {
12
+ ...blocksConfig,
13
+ ...config.blocks.blocksConfig,
14
+ };
15
+
16
+ jest.doMock('semantic-ui-react', () => ({
17
+ __esModule: true,
18
+ ...mockSemanticComponents,
19
+ Popup: ({ content, trigger }) => {
20
+ return (
21
+ <div className="popup">
22
+ <div className="trigger">{trigger}</div>
23
+ <div className="content">{content}</div>
24
+ </div>
25
+ );
26
+ },
27
+ }));
28
+
29
+ jest.doMock('@plone/volto/components', () => {
30
+ return {
31
+ __esModule: true,
32
+ ...mockComponents,
33
+ SidebarPortal: ({ children }) => <div id="sidebar">{children}</div>,
34
+ };
35
+ });
36
+
37
+ jest.doMock('@plone/volto/registry', () =>
38
+ [installSlate].reduce((acc, apply) => apply(acc), config),
39
+ );
40
+
41
+ const mockStore = configureStore([thunk]);
42
+
43
+ global.fetch = jest.fn(() =>
44
+ Promise.resolve({
45
+ json: () => Promise.resolve({}),
46
+ }),
47
+ );
48
+
49
+ global.store = mockStore({
50
+ intl: {
51
+ locale: 'en',
52
+ messages: {},
53
+ formatMessage: jest.fn(),
54
+ },
55
+ content: {
56
+ create: {},
57
+ subrequests: [],
58
+ },
59
+ connected_data_parameters: {},
60
+ screen: {
61
+ page: {
62
+ width: 768,
63
+ },
64
+ },
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "1.27.2",
3
+ "version": "1.28.1",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -32,6 +32,7 @@
32
32
  "@cypress/code-coverage": "^3.10.0",
33
33
  "@plone/scripts": "*",
34
34
  "babel-plugin-transform-class-properties": "^6.24.1",
35
+ "dotenv": "^16.3.2",
35
36
  "husky": "^8.0.3",
36
37
  "lint-staged": "^14.0.1",
37
38
  "md5": "^2.3.0",
@@ -0,0 +1,404 @@
1
+ import ReactDOM from 'react-dom';
2
+ import cx from 'classnames';
3
+ import { isEqual, cloneDeep } from 'lodash';
4
+ import { Transforms, Editor, Point } 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 './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 '@plone/volto-slate/editor/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
+ function resetNodes(editor, options = {}) {
52
+ const children = [...editor.children];
53
+
54
+ children.forEach((node) =>
55
+ editor.apply({ type: 'remove_node', path: [0], node }),
56
+ );
57
+
58
+ if (options.nodes) {
59
+ options.nodes.forEach((node, i) =>
60
+ editor.apply({ type: 'insert_node', path: [i], node: node }),
61
+ );
62
+ }
63
+
64
+ const point =
65
+ options.at && Point.isPoint(options.at)
66
+ ? options.at
67
+ : Editor.end(editor, []);
68
+
69
+ if (point) {
70
+ Transforms.select(editor, point);
71
+ }
72
+ }
73
+
74
+ // TODO: implement onFocus
75
+ class SlateEditor extends Component {
76
+ constructor(props) {
77
+ super(props);
78
+
79
+ this.createEditor = this.createEditor.bind(this);
80
+ this.multiDecorator = this.multiDecorator.bind(this);
81
+ this.handleChange = this.handleChange.bind(this);
82
+ this.getSavedSelection = this.getSavedSelection.bind(this);
83
+ this.setSavedSelection = this.setSavedSelection.bind(this);
84
+
85
+ this.savedSelection = null;
86
+
87
+ const uid = uuid(); // used to namespace the editor's plugins
88
+
89
+ this.slateSettings = props.slateSettings || config.settings.slate;
90
+
91
+ this.initialValue = cloneDeep(
92
+ this.props.value || this.slateSettings.defaultValue(),
93
+ );
94
+
95
+ this.state = {
96
+ editor: this.createEditor(uid),
97
+ showExpandedToolbar: config.settings.slate.showExpandedToolbar,
98
+ internalValue: this.initialValue,
99
+ uid,
100
+ };
101
+
102
+ this.editor = null;
103
+ this.selectionTimeout = null;
104
+ }
105
+
106
+ getSavedSelection() {
107
+ return this.savedSelection;
108
+ }
109
+ setSavedSelection(selection) {
110
+ this.savedSelection = selection;
111
+ }
112
+
113
+ createEditor(uid) {
114
+ // extensions are "editor plugins" or "editor wrappers". It's a similar
115
+ // similar to OOP inheritance, where a callable creates a new copy of the
116
+ // editor, while replacing or adding new capabilities to that editor.
117
+ // Extensions are purely JS, no React components.
118
+ const editor = makeEditor({ extensions: this.props.extensions });
119
+
120
+ // When the editor loses focus it no longer has a valid selections. This
121
+ // makes it impossible to have complex types of interactions (like filling
122
+ // in another text box, operating a select menu, etc). For this reason we
123
+ // save the active selection
124
+
125
+ editor.getSavedSelection = this.getSavedSelection;
126
+ editor.setSavedSelection = this.setSavedSelection;
127
+ editor.uid = uid || this.state.uid;
128
+
129
+ return editor;
130
+ }
131
+
132
+ handleChange(value) {
133
+ ReactDOM.unstable_batchedUpdates(() => {
134
+ const newValue = cloneDeep(value);
135
+ this.setState({ internalValue: newValue });
136
+ if (this.props.onChange && !isEqual(newValue, this.props.value)) {
137
+ this.props.onChange(newValue, this.editor);
138
+ }
139
+ });
140
+ }
141
+
142
+ multiDecorator([node, path]) {
143
+ // Decorations (such as higlighting node types, selection, etc).
144
+ const { runtimeDecorators = [] } = this.slateSettings;
145
+ return runtimeDecorators.reduce(
146
+ (acc, deco) => deco(this.state.editor, [node, path], acc),
147
+ [],
148
+ );
149
+ }
150
+
151
+ componentDidMount() {
152
+ // watch the dom change
153
+
154
+ if (this.props.selected) {
155
+ let focused = true;
156
+ try {
157
+ focused = ReactEditor.isFocused(this.state.editor);
158
+ } catch {}
159
+ if (!focused) {
160
+ setTimeout(() => {
161
+ try {
162
+ ReactEditor.focus(this.state.editor);
163
+ } catch {}
164
+ }, 100); // flush
165
+ }
166
+
167
+ this.state.editor.normalize({ force: true });
168
+ }
169
+ }
170
+
171
+ componentWillUnmount() {
172
+ this.isUnmounted = true;
173
+ }
174
+
175
+ componentDidUpdate(prevProps) {
176
+ if (!isEqual(prevProps.extensions, this.props.extensions)) {
177
+ this.setState({ editor: this.createEditor() });
178
+ return;
179
+ }
180
+
181
+ if (
182
+ this.props.value &&
183
+ !isEqual(this.props.value, this.state.internalValue)
184
+ ) {
185
+ const newValue = cloneDeep(this.props.value);
186
+ const { editor } = this.state;
187
+
188
+ resetNodes(editor, { nodes: newValue });
189
+
190
+ this.setState({
191
+ internalValue: newValue,
192
+ });
193
+
194
+ if (this.props.defaultSelection) {
195
+ const selection = parseDefaultSelection(
196
+ editor,
197
+ this.props.defaultSelection,
198
+ );
199
+
200
+ ReactEditor.focus(editor);
201
+ Transforms.select(editor, selection);
202
+ } else {
203
+ Transforms.select(editor, Editor.end(editor, []));
204
+ }
205
+ return;
206
+ }
207
+
208
+ const { editor } = this.state;
209
+
210
+ if (!prevProps.selected && this.props.selected) {
211
+ // if the SlateEditor becomes selected from unselected
212
+
213
+ if (window.getSelection().type === 'None') {
214
+ // TODO: why is this condition checked?
215
+ Transforms.select(
216
+ this.state.editor,
217
+ Editor.range(this.state.editor, Editor.start(this.state.editor, [])),
218
+ );
219
+ }
220
+
221
+ ReactEditor.focus(this.state.editor);
222
+ }
223
+
224
+ if (this.props.selected && this.props.onUpdate) {
225
+ this.props.onUpdate(editor);
226
+ }
227
+ }
228
+
229
+ shouldComponentUpdate(nextProps, nextState) {
230
+ const { selected = true, value, readOnly } = nextProps;
231
+ const res =
232
+ selected ||
233
+ this.props.selected !== selected ||
234
+ this.props.readOnly !== readOnly ||
235
+ !isEqual(value, this.props.value);
236
+ return res;
237
+ }
238
+
239
+ render() {
240
+ const {
241
+ selected,
242
+ placeholder,
243
+ onKeyDown,
244
+ testingEditorRef,
245
+ readOnly,
246
+ className,
247
+ renderExtensions = [],
248
+ editableProps = {},
249
+ } = this.props;
250
+ const slateSettings = this.slateSettings;
251
+
252
+ // renderExtensions is needed because the editor is memoized, so if these
253
+ // extensions need an updated state (for example to insert updated
254
+ // blockProps) then we need to always wrap the editor with them
255
+ const editor = renderExtensions.reduce(
256
+ (acc, apply) => apply(acc),
257
+ this.state.editor,
258
+ );
259
+
260
+ // Reset selection if field is reset
261
+ if (
262
+ editor.selection &&
263
+ this.props.value?.length === 1 &&
264
+ this.props.value[0].children.length === 1 &&
265
+ this.props.value[0].children[0].text === ''
266
+ ) {
267
+ Transforms.select(editor, {
268
+ anchor: { path: [0, 0], offset: 0 },
269
+ focus: { path: [0, 0], offset: 0 },
270
+ });
271
+ }
272
+ this.editor = editor;
273
+
274
+ if (testingEditorRef) {
275
+ testingEditorRef.current = editor;
276
+ }
277
+
278
+ // debug-values are `data-` HTML attributes in withTestingFeatures HOC
279
+
280
+ return (
281
+ <div
282
+ {...this.props['debug-values']}
283
+ className={cx('slate-editor', {
284
+ 'show-toolbar': this.state.showExpandedToolbar,
285
+ selected,
286
+ })}
287
+ tabIndex={-1}
288
+ >
289
+ <EditorContext.Provider value={editor}>
290
+ <Slate
291
+ editor={editor}
292
+ initialValue={this.initialValue}
293
+ onChange={this.handleChange}
294
+ >
295
+ {selected ? (
296
+ <>
297
+ <InlineToolbar
298
+ editor={editor}
299
+ className={className}
300
+ slateSettings={this.props.slateSettings}
301
+ />
302
+ {Object.keys(slateSettings.elementToolbarButtons).map(
303
+ (t, i) => {
304
+ return (
305
+ <Toolbar elementType={t} key={i}>
306
+ {slateSettings.elementToolbarButtons[t].map(
307
+ (Btn, b) => {
308
+ return <Btn editor={editor} key={b} />;
309
+ },
310
+ )}
311
+ </Toolbar>
312
+ );
313
+ },
314
+ )}
315
+ </>
316
+ ) : (
317
+ ''
318
+ )}
319
+ <Editable
320
+ tabIndex={this.props.tabIndex || 0}
321
+ readOnly={readOnly}
322
+ placeholder={placeholder}
323
+ renderElement={(props) => <Element {...props} />}
324
+ renderLeaf={(props) => <Leaf {...props} />}
325
+ decorate={this.multiDecorator}
326
+ spellCheck={false}
327
+ scrollSelectionIntoView={
328
+ slateSettings.scrollIntoView ? undefined : () => null
329
+ }
330
+ onBlur={() => {
331
+ this.props.onBlur && this.props.onBlur();
332
+ return null;
333
+ }}
334
+ onClick={this.props.onClick}
335
+ onSelect={(e) => {
336
+ if (!selected && this.props.onFocus) {
337
+ // we can't overwrite the onFocus of Editable, as the onFocus
338
+ // in Slate has too much builtin behaviour that's not
339
+ // accessible otherwise. Instead we try to detect such an
340
+ // event based on observing selected state
341
+ if (!editor.selection) {
342
+ setTimeout(() => {
343
+ this.props.onFocus();
344
+ }, 100); // TODO: why 100 is chosen here?
345
+ }
346
+ }
347
+
348
+ if (this.selectionTimeout) clearTimeout(this.selectionTimeout);
349
+ this.selectionTimeout = setTimeout(() => {
350
+ if (
351
+ editor.selection &&
352
+ !isEqual(editor.selection, this.savedSelection) &&
353
+ !this.isUnmounted
354
+ ) {
355
+ this.setState((state) => ({ update: !this.state.update }));
356
+ this.setSavedSelection(
357
+ JSON.parse(JSON.stringify(editor.selection)),
358
+ );
359
+ }
360
+ }, 200);
361
+ }}
362
+ onKeyDown={(event) => {
363
+ const handled = handleHotKeys(editor, event, slateSettings);
364
+ if (handled) return;
365
+ onKeyDown && onKeyDown({ editor, event });
366
+ }}
367
+ {...editableProps}
368
+ />
369
+ {selected &&
370
+ slateSettings.persistentHelpers.map((Helper, i) => {
371
+ return <Helper key={i} editor={editor} />;
372
+ })}
373
+ {this.props.debug ? (
374
+ <ul>
375
+ <li>{selected ? 'selected' : 'no-selected'}</li>
376
+ <li>
377
+ {ReactEditor.isFocused(editor) ? 'focused' : 'unfocused'}
378
+ </li>
379
+ <li>
380
+ savedSelection: {JSON.stringify(editor.getSavedSelection())}
381
+ </li>
382
+ <li>live selection: {JSON.stringify(editor.selection)}</li>
383
+ <li>children: {JSON.stringify(editor.children)}</li>
384
+ </ul>
385
+ ) : (
386
+ ''
387
+ )}
388
+ {this.props.children}
389
+ </Slate>
390
+ </EditorContext.Provider>
391
+ </div>
392
+ );
393
+ }
394
+ }
395
+
396
+ SlateEditor.defaultProps = {
397
+ extensions: [],
398
+ className: '',
399
+ };
400
+
401
+ // May be needed to wrap in React.memo(), it used to be wrapped in connect()
402
+ export default __CLIENT__ && window?.Cypress
403
+ ? withTestingFeatures(SlateEditor)
404
+ : SlateEditor;
@@ -70,10 +70,11 @@ export const Leaf = ({ children, ...rest }) => {
70
70
  typeof children === 'string' ? (
71
71
  children.split('\n').map((t, i) => {
72
72
  // Softbreak support. Should do a plugin?
73
+ const hasSoftBreak =
74
+ children.indexOf('\n') > -1 && children.split('\n').length - 1 > i;
73
75
  return (
74
76
  <React.Fragment key={`${i}`}>
75
- {children.indexOf('\n') > -1 &&
76
- children.split('\n').length - 1 > i ? (
77
+ {hasSoftBreak ? (
77
78
  <>
78
79
  {klass ? <span className={klass}>{t}</span> : t}
79
80
  <br />
@@ -104,7 +105,20 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => {
104
105
  const editor = { children: nodes || [] };
105
106
 
106
107
  const _serializeNodes = (nodes) => {
107
- return (nodes || []).map(([node, path], i) => {
108
+ return (nodes || []).map(([node, path]) => {
109
+ let _serialized;
110
+ const isTextNode = Text.isText(node);
111
+ try {
112
+ if (!isTextNode) {
113
+ _serialized = _serializeNodes(
114
+ Array.from(Node.children(editor, path)),
115
+ );
116
+ }
117
+ } catch {
118
+ // eslint-disable-next-line no-console
119
+ console.error('Error in serializing nodes', editor, path);
120
+ }
121
+
108
122
  return Text.isText(node) ? (
109
123
  <Leaf path={path} leaf={node} text={node} mode="view" key={path}>
110
124
  {node.text}
@@ -125,7 +139,7 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => {
125
139
  }
126
140
  extras={extras}
127
141
  >
128
- {_serializeNodes(Array.from(Node.children(editor, path)))}
142
+ {_serialized}
129
143
  </Element>
130
144
  );
131
145
  });
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ import { TokenWidget } from '@eeacms/volto-eea-website-theme/components/theme/Wi
8
8
  import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TopicsWidget';
9
9
  import { Icon } from '@plone/volto/components';
10
10
  import { getBlocks } from '@plone/volto/helpers';
11
+ import { serializeNodesToText } from '@plone/volto-slate/editor/render';
11
12
  import Tag from '@eeacms/volto-eea-design-system/ui/Tag/Tag';
12
13
 
13
14
  import {
@@ -224,6 +225,20 @@ const applyConfig = (config) => {
224
225
  ...config.views.errorViews,
225
226
  '404': NotFound,
226
227
  };
228
+ // Apply slate text block customization
229
+ if (config.blocks.blocksConfig.slate) {
230
+ config.blocks.blocksConfig.slate.tocEntry = (block = {}) => {
231
+ const { value, override_toc, entry_text, level } = block;
232
+ const plaintext =
233
+ serializeNodesToText(block.value || []) || block.plaintext;
234
+ const type = value?.[0]?.type;
235
+ return override_toc && level
236
+ ? [parseInt(level.slice(1)), entry_text]
237
+ : config.settings.slate.topLevelTargetElements.includes(type)
238
+ ? [parseInt(type.slice(1)), plaintext]
239
+ : null;
240
+ };
241
+ }
227
242
  // Apply accordion block customization
228
243
  if (config.blocks.blocksConfig.accordion) {
229
244
  config.blocks.blocksConfig.accordion.titleIcons = {