@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 +26 -17
- package/CHANGELOG.md +23 -1
- package/README.md +4 -0
- package/jest-addon.config.js +20 -11
- package/jest.setup.js +65 -0
- package/package.json +2 -1
- package/src/customizations/@plone/volto-slate/editor/SlateEditor.jsx +404 -0
- package/src/customizations/@plone/volto-slate/editor/render.jsx +18 -4
- package/src/index.js +15 -0
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
|
-
|
30
|
-
extends: `${
|
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.
|
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
|
package/jest-addon.config.js
CHANGED
@@ -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:
|
34
|
-
functions:
|
35
|
-
lines:
|
36
|
-
statements:
|
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.
|
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
|
-
{
|
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]
|
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
|
-
{
|
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 = {
|