@eeacms/volto-clms-theme 1.0.185 → 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 +17 -0
- package/package.json +1 -1
- package/src/components/CLMSDatasetDetailView/CLMSDatasetDetailView.jsx +186 -168
- package/src/components/CLMSDownloadCartView/CLMSCartContent.jsx +8 -8
- package/src/components/CLMSMeetingView/CLMSMeetingView.jsx +8 -2
- 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,23 @@ 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)]
|
|
14
|
+
### [1.0.186](https://github.com/eea/volto-clms-theme/compare/1.0.185...1.0.186) - 14 March 2023
|
|
15
|
+
|
|
16
|
+
#### :bug: Bug Fixes
|
|
17
|
+
|
|
18
|
+
- fix: formatConversionTable condition to avoid cart first loading error [ionlizarazu - [`054be8e`](https://github.com/eea/volto-clms-theme/commit/054be8e9603d481665d9efeefcb168288aebba40)]
|
|
19
|
+
- fix: remove icon title to avoid duplicated messages [ionlizarazu - [`d4f562a`](https://github.com/eea/volto-clms-theme/commit/d4f562ae4e66847bf02f80d0448e6515da1093e6)]
|
|
20
|
+
|
|
21
|
+
#### :hammer_and_wrench: Others
|
|
22
|
+
|
|
23
|
+
- logged in buttons for editor style [ionlizarazu - [`da560b0`](https://github.com/eea/volto-clms-theme/commit/da560b09e92029ae9cc0c193a35771ea4ec8bf1f)]
|
|
7
24
|
### [1.0.185](https://github.com/eea/volto-clms-theme/compare/1.0.184...1.0.185) - 13 March 2023
|
|
8
25
|
|
|
9
26
|
#### :hammer_and_wrench: Others
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
|
|
3
3
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
|
4
4
|
import { useLocation } from 'react-router-dom';
|
|
5
5
|
import { compose } from 'redux';
|
|
6
|
-
import { Modal, Segment } from 'semantic-ui-react';
|
|
6
|
+
import { Modal, Segment, Grid } from 'semantic-ui-react';
|
|
7
7
|
|
|
8
8
|
import { getUser } from '@plone/volto/actions';
|
|
9
9
|
import CclButton from '@eeacms/volto-clms-theme/components/CclButton/CclButton';
|
|
@@ -77,191 +77,209 @@ const CLMSDatasetDetailView = ({ content, token }) => {
|
|
|
77
77
|
{user?.roles && user.roles.includes('Manager') && (
|
|
78
78
|
<>
|
|
79
79
|
<h2>MapLayers importation options:</h2>
|
|
80
|
-
<Segment
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
className={'modal-clms'}
|
|
106
|
-
>
|
|
107
|
-
<div className={'modal-clms-background'}>
|
|
108
|
-
<div className={'modal-clms-container'}>
|
|
109
|
-
<div className={'modal-close modal-clms-close'}>
|
|
110
|
-
<span
|
|
111
|
-
className="ccl-icon-close"
|
|
112
|
-
aria-label="Close"
|
|
113
|
-
onClick={() => {
|
|
114
|
-
setOpen({ ...open, 'wms-layers-import': false });
|
|
115
|
-
}}
|
|
116
|
-
onKeyDown={() => {
|
|
117
|
-
setOpen({ ...open, 'wms-layers-import': false });
|
|
118
|
-
}}
|
|
119
|
-
tabIndex="0"
|
|
120
|
-
role="button"
|
|
121
|
-
></span>
|
|
122
|
-
</div>
|
|
123
|
-
<div className="modal-login-text">
|
|
124
|
-
<h1>
|
|
80
|
+
<Segment basic>
|
|
81
|
+
<Grid columns={2} compact horizontal>
|
|
82
|
+
<Grid.Column>
|
|
83
|
+
<Segment
|
|
84
|
+
padded={'very'}
|
|
85
|
+
color={'olive'}
|
|
86
|
+
key={'wms-layers-import'}
|
|
87
|
+
loading={wms_layers_importation?.loading}
|
|
88
|
+
style={{
|
|
89
|
+
width: '100%',
|
|
90
|
+
display: 'flex',
|
|
91
|
+
justifyContent: 'center',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<Modal
|
|
95
|
+
onClose={() => {
|
|
96
|
+
setOpen({ ...open, 'wms-layers-import': false });
|
|
97
|
+
}}
|
|
98
|
+
onOpen={() => {
|
|
99
|
+
setOpen({ ...open, 'wms-layers-import': true });
|
|
100
|
+
}}
|
|
101
|
+
open={open['wms-layers-import']}
|
|
102
|
+
trigger={
|
|
103
|
+
<CclButton>
|
|
125
104
|
<FormattedMessage
|
|
126
105
|
id="Import WMS Layers"
|
|
127
106
|
defaultMessage="Import WMS Layers"
|
|
128
107
|
/>
|
|
129
|
-
</
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
108
|
+
</CclButton>
|
|
109
|
+
}
|
|
110
|
+
className={'modal-clms'}
|
|
111
|
+
>
|
|
112
|
+
<div className={'modal-clms-background'}>
|
|
113
|
+
<div className={'modal-clms-container'}>
|
|
114
|
+
<div className={'modal-close modal-clms-close'}>
|
|
115
|
+
<span
|
|
116
|
+
className="ccl-icon-close"
|
|
117
|
+
aria-label="Close"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
setOpen({ ...open, 'wms-layers-import': false });
|
|
120
|
+
}}
|
|
121
|
+
onKeyDown={() => {
|
|
122
|
+
setOpen({ ...open, 'wms-layers-import': false });
|
|
123
|
+
}}
|
|
124
|
+
tabIndex="0"
|
|
125
|
+
role="button"
|
|
126
|
+
></span>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="modal-login-text">
|
|
129
|
+
<h1>
|
|
130
|
+
<FormattedMessage
|
|
131
|
+
id="Import WMS Layers"
|
|
132
|
+
defaultMessage="Import WMS Layers"
|
|
133
|
+
/>
|
|
134
|
+
</h1>
|
|
135
|
+
This action will import the WMS Layers from the view
|
|
136
|
+
service defined in the dataset or from GeoNetwork if
|
|
137
|
+
the view service is not defined and a linked
|
|
138
|
+
geonetwork record has a valid WMS service link
|
|
139
|
+
<br />
|
|
140
|
+
<br />
|
|
141
|
+
</div>
|
|
142
|
+
<CclButton
|
|
143
|
+
onClick={() => {
|
|
144
|
+
handleWMSImport();
|
|
145
|
+
setOpen({ ...open, 'wms-layers-import': false });
|
|
146
|
+
}}
|
|
147
|
+
mode="filled"
|
|
148
|
+
>
|
|
149
|
+
<FormattedMessage
|
|
150
|
+
id="Import data"
|
|
151
|
+
defaultMessage="Import data"
|
|
152
|
+
/>
|
|
153
|
+
</CclButton>
|
|
154
|
+
</div>
|
|
136
155
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
wms_layers_importation?.error === null && (
|
|
156
|
+
</Modal>
|
|
157
|
+
{wms_layers_importation?.imported_wms_layers?.status && (
|
|
158
|
+
<p>
|
|
159
|
+
{wms_layers_importation?.loaded &&
|
|
160
|
+
wms_layers_importation?.error === null && (
|
|
161
|
+
<strong>
|
|
162
|
+
{' '}
|
|
163
|
+
{
|
|
164
|
+
wms_layers_importation?.imported_wms_layers
|
|
165
|
+
?.message
|
|
166
|
+
}
|
|
167
|
+
</strong>
|
|
168
|
+
)}
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
{wms_layers_importation?.imported_wms_layers?.status ===
|
|
172
|
+
'error' && (
|
|
173
|
+
<p>
|
|
156
174
|
<strong>
|
|
157
175
|
{' '}
|
|
158
176
|
{wms_layers_importation?.imported_wms_layers?.message}
|
|
159
177
|
</strong>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
setOpen({ ...open, 'wms-fields-import': true });
|
|
187
|
-
}}
|
|
188
|
-
open={open['wms-fields-import']}
|
|
189
|
-
trigger={
|
|
190
|
-
<CclButton>
|
|
191
|
-
<FormattedMessage
|
|
192
|
-
id="Import WMS Fields"
|
|
193
|
-
defaultMessage="Import WMS Fields"
|
|
194
|
-
/>
|
|
195
|
-
</CclButton>
|
|
196
|
-
}
|
|
197
|
-
className={'modal-clms'}
|
|
198
|
-
>
|
|
199
|
-
<div className={'modal-clms-background'}>
|
|
200
|
-
<div className={'modal-clms-container'}>
|
|
201
|
-
<div className={'modal-close modal-clms-close'}>
|
|
202
|
-
<span
|
|
203
|
-
className="ccl-icon-close"
|
|
204
|
-
aria-label="Close"
|
|
205
|
-
onClick={() => {
|
|
206
|
-
setOpen({ ...open, 'wms-fields-import': false });
|
|
207
|
-
}}
|
|
208
|
-
onKeyDown={() => {
|
|
209
|
-
setOpen({ ...open, 'wms-fields-import': false });
|
|
210
|
-
}}
|
|
211
|
-
tabIndex="0"
|
|
212
|
-
role="button"
|
|
213
|
-
></span>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="modal-login-text">
|
|
216
|
-
<h1>
|
|
178
|
+
</p>
|
|
179
|
+
)}
|
|
180
|
+
</Segment>
|
|
181
|
+
</Grid.Column>
|
|
182
|
+
<Grid.Column>
|
|
183
|
+
<Segment
|
|
184
|
+
padded={'very'}
|
|
185
|
+
color={'olive'}
|
|
186
|
+
key={'wms-fields-import'}
|
|
187
|
+
loading={wms_fields_importation?.loading}
|
|
188
|
+
style={{
|
|
189
|
+
width: '100%',
|
|
190
|
+
display: 'flex',
|
|
191
|
+
justifyContent: 'center',
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<Modal
|
|
195
|
+
onClose={() => {
|
|
196
|
+
setOpen({ ...open, 'wms-fields-import': false });
|
|
197
|
+
}}
|
|
198
|
+
onOpen={() => {
|
|
199
|
+
setOpen({ ...open, 'wms-fields-import': true });
|
|
200
|
+
}}
|
|
201
|
+
open={open['wms-fields-import']}
|
|
202
|
+
trigger={
|
|
203
|
+
<CclButton>
|
|
217
204
|
<FormattedMessage
|
|
218
205
|
id="Import WMS Fields"
|
|
219
206
|
defaultMessage="Import WMS Fields"
|
|
220
207
|
/>
|
|
221
|
-
</
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<
|
|
208
|
+
</CclButton>
|
|
209
|
+
}
|
|
210
|
+
className={'modal-clms'}
|
|
211
|
+
>
|
|
212
|
+
<div className={'modal-clms-background'}>
|
|
213
|
+
<div className={'modal-clms-container'}>
|
|
214
|
+
<div className={'modal-close modal-clms-close'}>
|
|
215
|
+
<span
|
|
216
|
+
className="ccl-icon-close"
|
|
217
|
+
aria-label="Close"
|
|
218
|
+
onClick={() => {
|
|
219
|
+
setOpen({ ...open, 'wms-fields-import': false });
|
|
220
|
+
}}
|
|
221
|
+
onKeyDown={() => {
|
|
222
|
+
setOpen({ ...open, 'wms-fields-import': false });
|
|
223
|
+
}}
|
|
224
|
+
tabIndex="0"
|
|
225
|
+
role="button"
|
|
226
|
+
></span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="modal-login-text">
|
|
229
|
+
<h1>
|
|
230
|
+
<FormattedMessage
|
|
231
|
+
id="Import WMS Fields"
|
|
232
|
+
defaultMessage="Import WMS Fields"
|
|
233
|
+
/>
|
|
234
|
+
</h1>
|
|
235
|
+
This action will import the WMS Fields from the view
|
|
236
|
+
service defined in the dataset if this WMS service is
|
|
237
|
+
Arcgis based
|
|
238
|
+
<br />
|
|
239
|
+
<br />
|
|
240
|
+
</div>
|
|
241
|
+
<CclButton
|
|
242
|
+
onClick={() => {
|
|
243
|
+
handleWMSFieldimport();
|
|
244
|
+
setOpen({ ...open, 'wms-fields-import': false });
|
|
245
|
+
}}
|
|
246
|
+
mode="filled"
|
|
247
|
+
>
|
|
248
|
+
<FormattedMessage
|
|
249
|
+
id="Import data"
|
|
250
|
+
defaultMessage="Import data"
|
|
251
|
+
/>
|
|
252
|
+
</CclButton>
|
|
253
|
+
</div>
|
|
227
254
|
</div>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
wms_fields_importation?.error === null && (
|
|
255
|
+
</Modal>
|
|
256
|
+
{wms_fields_importation?.imported_wms_fields?.status && (
|
|
257
|
+
<p>
|
|
258
|
+
{wms_fields_importation?.loaded &&
|
|
259
|
+
wms_fields_importation?.error === null && (
|
|
260
|
+
<strong>
|
|
261
|
+
{' '}
|
|
262
|
+
{
|
|
263
|
+
wms_fields_importation?.imported_wms_fields
|
|
264
|
+
?.message
|
|
265
|
+
}
|
|
266
|
+
</strong>
|
|
267
|
+
)}
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
{wms_fields_importation?.imported_wms_fields?.status ===
|
|
271
|
+
'error' && (
|
|
272
|
+
<p>
|
|
247
273
|
<strong>
|
|
248
274
|
{' '}
|
|
249
275
|
{wms_fields_importation?.imported_wms_fields?.message}
|
|
250
276
|
</strong>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
<strong>
|
|
258
|
-
{' '}
|
|
259
|
-
{wms_fields_importation?.imported_wms_fields?.message}
|
|
260
|
-
</strong>
|
|
261
|
-
</p>
|
|
262
|
-
)}
|
|
263
|
-
</Segment>
|
|
264
|
-
</Segment.Group>
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
</Segment>
|
|
280
|
+
</Grid.Column>
|
|
281
|
+
</Grid>
|
|
282
|
+
</Segment>
|
|
265
283
|
</>
|
|
266
284
|
)}
|
|
267
285
|
|
|
@@ -287,7 +287,7 @@ const CLMSCartContent = (props) => {
|
|
|
287
287
|
getCollectionByItem(item).collection ?? '-'
|
|
288
288
|
);
|
|
289
289
|
};
|
|
290
|
-
const FormatNaming = ({ item }) => {
|
|
290
|
+
const FormatNaming = ({ item, formatConversionTable }) => {
|
|
291
291
|
const format_options = getAvailableConversion(
|
|
292
292
|
formatConversionTable,
|
|
293
293
|
originalFormatNaming(item),
|
|
@@ -405,7 +405,12 @@ const CLMSCartContent = (props) => {
|
|
|
405
405
|
<CollectionNaming item={item} />
|
|
406
406
|
</td>
|
|
407
407
|
<td className="table-td-format">
|
|
408
|
-
|
|
408
|
+
{formatConversionTable && item && (
|
|
409
|
+
<FormatNaming
|
|
410
|
+
item={item}
|
|
411
|
+
formatConversionTable={formatConversionTable}
|
|
412
|
+
/>
|
|
413
|
+
)}
|
|
409
414
|
</td>
|
|
410
415
|
<td className="table-td-projections">
|
|
411
416
|
{!item.file_id ? (
|
|
@@ -455,11 +460,7 @@ const CLMSCartContent = (props) => {
|
|
|
455
460
|
outline: 'inherit',
|
|
456
461
|
}}
|
|
457
462
|
>
|
|
458
|
-
<Icon
|
|
459
|
-
name={addDocumentSVG}
|
|
460
|
-
size={25}
|
|
461
|
-
title={'Add a duplicated row below'}
|
|
462
|
-
/>
|
|
463
|
+
<Icon name={addDocumentSVG} size={25} />
|
|
463
464
|
</button>
|
|
464
465
|
</span>
|
|
465
466
|
) : (
|
|
@@ -493,7 +494,6 @@ const CLMSCartContent = (props) => {
|
|
|
493
494
|
name={removeSVG}
|
|
494
495
|
size={25}
|
|
495
496
|
color="#e40166"
|
|
496
|
-
title={'Remove this row from the cart'}
|
|
497
497
|
/>
|
|
498
498
|
</button>
|
|
499
499
|
</span>
|
|
@@ -314,13 +314,19 @@ export const CLMSMeetingView = (props) => {
|
|
|
314
314
|
</strong>
|
|
315
315
|
<br />
|
|
316
316
|
<br />
|
|
317
|
-
<CclButton
|
|
317
|
+
<CclButton
|
|
318
|
+
url={location.pathname + '/subscribers'}
|
|
319
|
+
style={{ marginLeft: '10px', marginRight: '10px' }}
|
|
320
|
+
>
|
|
318
321
|
<FormattedMessage
|
|
319
322
|
id="Participants"
|
|
320
323
|
defaultMessage="Participants"
|
|
321
324
|
/>
|
|
322
325
|
</CclButton>
|
|
323
|
-
<CclButton
|
|
326
|
+
<CclButton
|
|
327
|
+
url={location.pathname + '/emails'}
|
|
328
|
+
style={{ marginLeft: '10px', marginRight: '10px' }}
|
|
329
|
+
>
|
|
324
330
|
<FormattedMessage
|
|
325
331
|
id="Mail archive"
|
|
326
332
|
defaultMessage="Mail archive"
|
|
@@ -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
|