@eeacms/volto-group-block 8.1.0 → 9.0.0
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 +11 -3
- package/package.json +1 -1
- package/src/components/manage/Blocks/Group/CounterComponent.jsx +13 -4
- package/src/components/manage/Blocks/Group/CounterComponent.test.jsx +115 -0
- package/src/components/manage/Blocks/Group/DefaultBody.jsx +26 -8
- package/src/components/manage/Blocks/Group/Edit.jsx +45 -18
- package/src/components/manage/Blocks/Group/LayoutSchema.jsx +8 -0
- package/src/components/manage/Blocks/Group/editor.less +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,15 +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
|
-
### [
|
|
7
|
+
### [9.0.0](https://github.com/eea/volto-group-block/compare/8.1.1...9.0.0) - 10 February 2026
|
|
8
8
|
|
|
9
9
|
#### :rocket: New Features
|
|
10
10
|
|
|
11
|
-
- feat:
|
|
11
|
+
- feat: Persist charCount to block data and add maxCharsOverflowPercent - refs #294806 [Alin Voinea - [`b5bbd75`](https://github.com/eea/volto-group-block/commit/b5bbd7516cb8cb6bcff8541d711fd9fa74a4d92c)]
|
|
12
|
+
|
|
13
|
+
### [8.1.1](https://github.com/eea/volto-group-block/compare/8.1.0...8.1.1) - 26 January 2026
|
|
12
14
|
|
|
13
15
|
#### :hammer_and_wrench: Others
|
|
14
16
|
|
|
15
|
-
-
|
|
17
|
+
- Fix blocks chooser layout when inside section block [Teodor - [`978ba35`](https://github.com/eea/volto-group-block/commit/978ba35214fc0b22bca04f22f051fdcd25d9812a)]
|
|
18
|
+
### [8.1.0](https://github.com/eea/volto-group-block/compare/8.0.0...8.1.0) - 3 December 2025
|
|
19
|
+
|
|
20
|
+
#### :rocket: New Features
|
|
21
|
+
|
|
22
|
+
- feat: Add possibility to skip blocks from character counting - refs #293783 [Teodor Voicu - [`c35d76b`](https://github.com/eea/volto-group-block/commit/c35d76bd03476bbcf8324b247ecf909c093d4117)]
|
|
23
|
+
|
|
16
24
|
## [8.0.0](https://github.com/eea/volto-group-block/compare/7.1.1...8.0.0) - 3 December 2025
|
|
17
25
|
|
|
18
26
|
#### :boom: Breaking Change
|
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@ const countTextInEachBlock =
|
|
|
56
56
|
: countCharsWithSpaces(foundText);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
|
|
59
|
+
export const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
|
|
60
60
|
const { countTextIn, skipBlocksInGroups = [] } =
|
|
61
61
|
config.blocks?.blocksConfig?.group || {};
|
|
62
62
|
// use obj ref to update value - if you send number it will not be updated
|
|
@@ -82,12 +82,21 @@ const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
85
|
+
const maxChars = parseInt(data.maxChars) || 0;
|
|
86
|
+
const maxCharsOverflowPercent = parseInt(data.maxCharsOverflowPercent) || 0;
|
|
87
|
+
const { ignoreSpaces } = data;
|
|
88
|
+
const charCount =
|
|
89
|
+
data.charCount ?? countTextInBlocks(data?.data, ignoreSpaces, maxChars);
|
|
90
|
+
|
|
91
|
+
const overflowLimit =
|
|
92
|
+
maxCharsOverflowPercent > 0
|
|
93
|
+
? Math.ceil((maxChars * (100 + maxCharsOverflowPercent)) / 100)
|
|
94
|
+
: maxChars;
|
|
95
|
+
|
|
87
96
|
const counterClass =
|
|
88
97
|
charCount < Math.ceil(maxChars / 1.05)
|
|
89
98
|
? 'info'
|
|
90
|
-
: charCount
|
|
99
|
+
: charCount <= overflowLimit
|
|
91
100
|
? 'warning'
|
|
92
101
|
: 'danger';
|
|
93
102
|
|
|
@@ -231,4 +231,119 @@ describe('CounterComponent', () => {
|
|
|
231
231
|
expect(setSidebarTab).toHaveBeenCalledWith(1);
|
|
232
232
|
expect(setSelectedBlock).toHaveBeenCalled();
|
|
233
233
|
});
|
|
234
|
+
|
|
235
|
+
it('should render warning class when chars are within overflow zone (maxCharsOverflowPercent=10)', () => {
|
|
236
|
+
const { container, getByText } = render(
|
|
237
|
+
<CounterComponent
|
|
238
|
+
data={{
|
|
239
|
+
maxChars: 100,
|
|
240
|
+
maxCharsOverflowPercent: 10,
|
|
241
|
+
data: {
|
|
242
|
+
blocks: {
|
|
243
|
+
block1: { '@type': 'text', plaintext: 'a'.repeat(105) },
|
|
244
|
+
},
|
|
245
|
+
blocks_layout: {
|
|
246
|
+
items: ['block1'],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
}}
|
|
250
|
+
setSidebarTab={setSidebarTab}
|
|
251
|
+
setSelectedBlock={setSelectedBlock}
|
|
252
|
+
/>,
|
|
253
|
+
);
|
|
254
|
+
expect(getByText('5 characters over the limit')).toBeInTheDocument();
|
|
255
|
+
expect(container.querySelector('.counter.warning')).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should render danger class when chars exceed overflow zone (maxCharsOverflowPercent=10)', () => {
|
|
259
|
+
const { container, getByText } = render(
|
|
260
|
+
<CounterComponent
|
|
261
|
+
data={{
|
|
262
|
+
maxChars: 100,
|
|
263
|
+
maxCharsOverflowPercent: 10,
|
|
264
|
+
data: {
|
|
265
|
+
blocks: {
|
|
266
|
+
block1: { '@type': 'text', plaintext: 'a'.repeat(111) },
|
|
267
|
+
},
|
|
268
|
+
blocks_layout: {
|
|
269
|
+
items: ['block1'],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
}}
|
|
273
|
+
setSidebarTab={setSidebarTab}
|
|
274
|
+
setSelectedBlock={setSelectedBlock}
|
|
275
|
+
/>,
|
|
276
|
+
);
|
|
277
|
+
expect(getByText('11 characters over the limit')).toBeInTheDocument();
|
|
278
|
+
expect(container.querySelector('.counter.danger')).toBeInTheDocument();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should render warning class when chars equal maxChars with no overflow percent', () => {
|
|
282
|
+
const { container, getByText } = render(
|
|
283
|
+
<CounterComponent
|
|
284
|
+
data={{
|
|
285
|
+
maxChars: 100,
|
|
286
|
+
data: {
|
|
287
|
+
blocks: {
|
|
288
|
+
block1: { '@type': 'text', plaintext: 'a'.repeat(100) },
|
|
289
|
+
},
|
|
290
|
+
blocks_layout: {
|
|
291
|
+
items: ['block1'],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}}
|
|
295
|
+
setSidebarTab={setSidebarTab}
|
|
296
|
+
setSelectedBlock={setSelectedBlock}
|
|
297
|
+
/>,
|
|
298
|
+
);
|
|
299
|
+
expect(getByText('0 characters remaining out of 100')).toBeInTheDocument();
|
|
300
|
+
expect(container.querySelector('.counter.warning')).toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should render danger class when negative overflow percent falls back to maxChars', () => {
|
|
304
|
+
const { container, getByText } = render(
|
|
305
|
+
<CounterComponent
|
|
306
|
+
data={{
|
|
307
|
+
maxChars: 100,
|
|
308
|
+
maxCharsOverflowPercent: -5,
|
|
309
|
+
data: {
|
|
310
|
+
blocks: {
|
|
311
|
+
block1: { '@type': 'text', plaintext: 'a'.repeat(104) },
|
|
312
|
+
},
|
|
313
|
+
blocks_layout: {
|
|
314
|
+
items: ['block1'],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}}
|
|
318
|
+
setSidebarTab={setSidebarTab}
|
|
319
|
+
setSelectedBlock={setSelectedBlock}
|
|
320
|
+
/>,
|
|
321
|
+
);
|
|
322
|
+
expect(getByText('4 characters over the limit')).toBeInTheDocument();
|
|
323
|
+
expect(container.querySelector('.counter.danger')).toBeInTheDocument();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should use persisted charCount from data instead of recomputing', () => {
|
|
327
|
+
const { container, getByText } = render(
|
|
328
|
+
<CounterComponent
|
|
329
|
+
data={{
|
|
330
|
+
maxChars: 100,
|
|
331
|
+
charCount: 42,
|
|
332
|
+
data: {
|
|
333
|
+
blocks: {
|
|
334
|
+
block1: { '@type': 'text', plaintext: 'a'.repeat(90) },
|
|
335
|
+
},
|
|
336
|
+
blocks_layout: {
|
|
337
|
+
items: ['block1'],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
}}
|
|
341
|
+
setSidebarTab={setSidebarTab}
|
|
342
|
+
setSelectedBlock={setSelectedBlock}
|
|
343
|
+
/>,
|
|
344
|
+
);
|
|
345
|
+
// Should use charCount=42 from data, not 90 from blocks
|
|
346
|
+
expect(getByText('58 characters remaining out of 100')).toBeInTheDocument();
|
|
347
|
+
expect(container.querySelector('.counter.info')).toBeInTheDocument();
|
|
348
|
+
});
|
|
234
349
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Button } from 'semantic-ui-react';
|
|
2
2
|
import { BlocksForm, Icon, RenderBlocks } from '@plone/volto/components';
|
|
3
3
|
import EditBlockWrapper from './EditBlockWrapper';
|
|
4
|
+
import { countTextInBlocks } from './CounterComponent';
|
|
4
5
|
import { useLocation } from 'react-router-dom';
|
|
5
6
|
|
|
6
7
|
import helpSVG from '@plone/volto/icons/help.svg';
|
|
@@ -48,21 +49,38 @@ const GroupBlockDefaultBody = (props) => {
|
|
|
48
49
|
onSelectBlock(id, isMultipleSelection, e, selectedBlock);
|
|
49
50
|
}}
|
|
50
51
|
onChangeFormData={(newFormData) => {
|
|
51
|
-
|
|
52
|
+
const newData = {
|
|
52
53
|
...data,
|
|
53
54
|
data: newFormData,
|
|
54
|
-
}
|
|
55
|
+
};
|
|
56
|
+
if (data.maxChars) {
|
|
57
|
+
newData.charCount = countTextInBlocks(
|
|
58
|
+
newFormData,
|
|
59
|
+
data.ignoreSpaces,
|
|
60
|
+
data.maxChars,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
onChangeBlock(block, newData);
|
|
55
64
|
}}
|
|
56
65
|
onChangeField={(id, value) => {
|
|
57
66
|
if (['blocks', 'blocks_layout'].indexOf(id) > -1) {
|
|
58
67
|
blockState[id] = value;
|
|
59
|
-
|
|
68
|
+
const newChildData = {
|
|
69
|
+
...data.data,
|
|
70
|
+
...blockState,
|
|
71
|
+
};
|
|
72
|
+
const newData = {
|
|
60
73
|
...data,
|
|
61
|
-
data:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
data: newChildData,
|
|
75
|
+
};
|
|
76
|
+
if (data.maxChars) {
|
|
77
|
+
newData.charCount = countTextInBlocks(
|
|
78
|
+
newChildData,
|
|
79
|
+
data.ignoreSpaces,
|
|
80
|
+
data.maxChars,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
onChangeBlock(block, newData);
|
|
66
84
|
} else {
|
|
67
85
|
onChangeField(id, value);
|
|
68
86
|
}
|
|
@@ -18,7 +18,7 @@ import PropTypes from 'prop-types';
|
|
|
18
18
|
import { Segment } from 'semantic-ui-react';
|
|
19
19
|
import EditSchema from './EditSchema';
|
|
20
20
|
|
|
21
|
-
import CounterComponent from './CounterComponent';
|
|
21
|
+
import CounterComponent, { countTextInBlocks } from './CounterComponent';
|
|
22
22
|
import './editor.less';
|
|
23
23
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
24
24
|
import { compose } from 'redux';
|
|
@@ -115,20 +115,29 @@ const Edit = (props) => {
|
|
|
115
115
|
});
|
|
116
116
|
const selectedIndex =
|
|
117
117
|
data.data.blocks_layout.items.indexOf(selectedBlock) + 1;
|
|
118
|
-
|
|
119
|
-
...data,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
...data.data.blocks_layout.items.slice(selectedIndex),
|
|
128
|
-
],
|
|
129
|
-
},
|
|
118
|
+
const newChildData = {
|
|
119
|
+
...data?.data,
|
|
120
|
+
...newBlockData,
|
|
121
|
+
blocks_layout: {
|
|
122
|
+
items: [
|
|
123
|
+
...data.data.blocks_layout.items.slice(0, selectedIndex),
|
|
124
|
+
...pastedBlocks,
|
|
125
|
+
...data.data.blocks_layout.items.slice(selectedIndex),
|
|
126
|
+
],
|
|
130
127
|
},
|
|
131
|
-
}
|
|
128
|
+
};
|
|
129
|
+
const newData = {
|
|
130
|
+
...data,
|
|
131
|
+
data: newChildData,
|
|
132
|
+
};
|
|
133
|
+
if (data.maxChars) {
|
|
134
|
+
newData.charCount = countTextInBlocks(
|
|
135
|
+
newChildData,
|
|
136
|
+
data.ignoreSpaces,
|
|
137
|
+
data.maxChars,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
onChangeBlock(block, newData);
|
|
132
141
|
};
|
|
133
142
|
|
|
134
143
|
React.useEffect(() => {
|
|
@@ -137,10 +146,14 @@ const Edit = (props) => {
|
|
|
137
146
|
childBlocksForm.blocks_layout.items[0] !== selectedBlock
|
|
138
147
|
) {
|
|
139
148
|
setSelectedBlock(childBlocksForm.blocks_layout.items[0]);
|
|
140
|
-
|
|
149
|
+
const newData = {
|
|
141
150
|
...data,
|
|
142
151
|
data: childBlocksForm,
|
|
143
|
-
}
|
|
152
|
+
};
|
|
153
|
+
if (data.maxChars) {
|
|
154
|
+
newData.charCount = 0;
|
|
155
|
+
}
|
|
156
|
+
onChangeBlock(block, newData);
|
|
144
157
|
}
|
|
145
158
|
}, [onChangeBlock, childBlocksForm, selectedBlock, block, data, data_blocks]);
|
|
146
159
|
|
|
@@ -218,10 +231,24 @@ const Edit = (props) => {
|
|
|
218
231
|
title={props.intl.formatMessage(messages.sectionGroupSettings)}
|
|
219
232
|
formData={data}
|
|
220
233
|
onChangeField={(id, value) => {
|
|
221
|
-
|
|
234
|
+
const newData = {
|
|
222
235
|
...props.data,
|
|
223
236
|
[id]: value,
|
|
224
|
-
}
|
|
237
|
+
};
|
|
238
|
+
const effectiveMaxChars =
|
|
239
|
+
id === 'maxChars' ? value : props.data.maxChars;
|
|
240
|
+
const effectiveIgnoreSpaces =
|
|
241
|
+
id === 'ignoreSpaces' ? value : props.data.ignoreSpaces;
|
|
242
|
+
if (effectiveMaxChars) {
|
|
243
|
+
newData.charCount = countTextInBlocks(
|
|
244
|
+
props.data?.data,
|
|
245
|
+
effectiveIgnoreSpaces,
|
|
246
|
+
effectiveMaxChars,
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
delete newData.charCount;
|
|
250
|
+
}
|
|
251
|
+
props.onChangeBlock(props.block, newData);
|
|
225
252
|
}}
|
|
226
253
|
/>
|
|
227
254
|
)}
|
|
@@ -11,6 +11,7 @@ const Schema = {
|
|
|
11
11
|
'allowedBlocks',
|
|
12
12
|
'as',
|
|
13
13
|
'maxChars',
|
|
14
|
+
'maxCharsOverflowPercent',
|
|
14
15
|
'ignoreSpaces',
|
|
15
16
|
'readOnlySettings',
|
|
16
17
|
'disableInnerButtons',
|
|
@@ -68,6 +69,13 @@ const Schema = {
|
|
|
68
69
|
type: 'integer',
|
|
69
70
|
factory: 'Integer',
|
|
70
71
|
},
|
|
72
|
+
maxCharsOverflowPercent: {
|
|
73
|
+
title: 'Maximum Characters Overflow Percent',
|
|
74
|
+
description:
|
|
75
|
+
'The percentage of characters that can overflow the maximum characters limit before showing a warning: 0-100',
|
|
76
|
+
type: 'integer',
|
|
77
|
+
factory: 'Integer',
|
|
78
|
+
},
|
|
71
79
|
ignoreSpaces: {
|
|
72
80
|
title: 'Ignore spaces',
|
|
73
81
|
description: 'Ignore spaces while calculating maximum characters',
|