@plone/volto 17.20.3 → 17.21.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/.readthedocs.yaml +4 -5
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Text/extensions/breakListInWidget.js +67 -0
- package/packages/volto-slate/src/blocks/Text/extensions/index.js +1 -0
- package/packages/volto-slate/src/blocks/Text/index.js +2 -0
- package/packages/volto-slate/src/widgets/HtmlSlateWidget.jsx +9 -2
- package/packages/volto-slate/src/widgets/RichTextWidget.jsx +6 -0
- package/src/components/manage/Contents/Contents.jsx +6 -0
- package/src/components/manage/Widgets/CheckboxWidget.jsx +1 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +23 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +135 -0
- package/src/components/theme/Image/Image.jsx +8 -1
- package/src/components/theme/View/View.jsx +2 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.d.ts +1 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.d.ts +1 -0
package/.readthedocs.yaml
CHANGED
|
@@ -3,8 +3,7 @@ build:
|
|
|
3
3
|
os: "ubuntu-22.04"
|
|
4
4
|
tools:
|
|
5
5
|
python: "3.12"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- exit 183;
|
|
6
|
+
commands:
|
|
7
|
+
# Cancel the Read the Docs build
|
|
8
|
+
# https://docs.readthedocs.io/en/stable/build-customization.html#cancel-build-based-on-a-condition
|
|
9
|
+
- exit 183;
|
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,24 @@ myst:
|
|
|
17
17
|
|
|
18
18
|
<!-- towncrier release notes start -->
|
|
19
19
|
|
|
20
|
+
## 17.21.0 (2025-03-05)
|
|
21
|
+
|
|
22
|
+
### Feature
|
|
23
|
+
|
|
24
|
+
- Provide language alternate links @erral [#6616](https://github.com/plone/volto/issues/6616)
|
|
25
|
+
|
|
26
|
+
### Bugfix
|
|
27
|
+
|
|
28
|
+
- In `RichTextWidget` and `HtmlSlateWidget`, fix breaking a list by typing Enter. @nileshgulia1 [#6570](https://github.com/plone/volto/issues/6570)
|
|
29
|
+
- Display the appropriately sized image to eliminate blurring from upsizing smaller images in the `srcSet` generation. @giuliaghisini [#6637](https://github.com/plone/volto/issues/6637)
|
|
30
|
+
- a11y - Added id attribute to checkbox widget for proper identification and fixes label functionality for screen readers. @Wagner3UB [#6771](https://github.com/plone/volto/issues/6771)
|
|
31
|
+
|
|
32
|
+
## 17.20.4 (2025-01-07)
|
|
33
|
+
|
|
34
|
+
### Bugfix
|
|
35
|
+
|
|
36
|
+
- Fixed folder contents issues with persistent selection across page changes. @pnicolli [#6555](https://github.com/plone/volto/issues/6555)
|
|
37
|
+
|
|
20
38
|
## 17.20.3 (2024-12-12)
|
|
21
39
|
|
|
22
40
|
### Bugfix
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Editor, Range, Transforms } from 'slate';
|
|
2
|
+
|
|
3
|
+
import config from '@plone/volto/registry';
|
|
4
|
+
import { isCursorAtBlockEnd } from '@plone/volto-slate/utils/selection';
|
|
5
|
+
import { getCurrentListItem } from '@plone/volto-slate/utils/lists';
|
|
6
|
+
import { createEmptyParagraph } from '@plone/volto-slate/utils/blocks';
|
|
7
|
+
|
|
8
|
+
export const breakListInWidget = (editor) => {
|
|
9
|
+
const { insertBreak } = editor;
|
|
10
|
+
|
|
11
|
+
editor.insertBreak = () => {
|
|
12
|
+
if (!(editor.selection && Range.isCollapsed(editor.selection))) {
|
|
13
|
+
insertBreak();
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { slate } = config.settings;
|
|
18
|
+
const { anchor } = editor.selection;
|
|
19
|
+
|
|
20
|
+
const ref = Editor.rangeRef(editor, editor.selection, {
|
|
21
|
+
affinity: 'inward',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const [listItem, listItemPath] = getCurrentListItem(editor);
|
|
25
|
+
if (listItem) {
|
|
26
|
+
if (Editor.string(editor, listItemPath)) {
|
|
27
|
+
Transforms.splitNodes(editor, {
|
|
28
|
+
at: editor.selection,
|
|
29
|
+
match: (node) => node.type === slate.listItemType,
|
|
30
|
+
always: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [parent] = Editor.parent(editor, anchor.path);
|
|
38
|
+
|
|
39
|
+
if (parent.type !== slate.listItemType || anchor.offset > 0) {
|
|
40
|
+
insertBreak();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Editor.deleteBackward(editor, { unit: 'line' });
|
|
45
|
+
// also account for empty nodes [{text: ''}]
|
|
46
|
+
if (Editor.isEmpty(editor, parent)) {
|
|
47
|
+
Transforms.removeNodes(editor, { at: ref.current });
|
|
48
|
+
|
|
49
|
+
Transforms.insertNodes(editor, createEmptyParagraph(), {
|
|
50
|
+
at: [editor.children.length],
|
|
51
|
+
});
|
|
52
|
+
Transforms.select(editor, Editor.end(editor, []));
|
|
53
|
+
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Transforms.removeNodes(editor, { at: ref.current });
|
|
58
|
+
|
|
59
|
+
if (isCursorAtBlockEnd(editor)) {
|
|
60
|
+
Editor.insertNode(editor, createEmptyParagraph());
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return editor;
|
|
67
|
+
};
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { withDeleteSelectionOnEnter } from '@plone/volto-slate/editor/extensions';
|
|
24
24
|
import {
|
|
25
25
|
breakList,
|
|
26
|
+
breakListInWidget,
|
|
26
27
|
withDeserializers,
|
|
27
28
|
withLists,
|
|
28
29
|
withSplitBlocksOnBreak,
|
|
@@ -48,6 +49,7 @@ export default function applyConfig(config) {
|
|
|
48
49
|
breakList,
|
|
49
50
|
normalizeExternalData,
|
|
50
51
|
],
|
|
52
|
+
slateWidgetExtensions: [breakListInWidget],
|
|
51
53
|
|
|
52
54
|
// Pluggable handlers for the onKeyDown event of <Editable />
|
|
53
55
|
// Order matters here. A handler can return `true` to stop executing any
|
|
@@ -12,6 +12,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|
|
12
12
|
import { FormFieldWrapper } from '@plone/volto/components';
|
|
13
13
|
import SlateEditor from '@plone/volto-slate/editor/SlateEditor';
|
|
14
14
|
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
15
|
+
import { handleKeyDetached } from '@plone/volto-slate/blocks/Text/keyboard';
|
|
15
16
|
import { makeEditor } from '@plone/volto-slate/utils';
|
|
16
17
|
import deserialize from '@plone/volto-slate/editor/deserialize';
|
|
17
18
|
|
|
@@ -19,14 +20,15 @@ import {
|
|
|
19
20
|
createEmptyParagraph,
|
|
20
21
|
normalizeExternalData,
|
|
21
22
|
} from '@plone/volto-slate/utils';
|
|
23
|
+
import config from '@plone/volto/registry';
|
|
24
|
+
|
|
22
25
|
import { ErrorBoundary } from './ErrorBoundary';
|
|
23
26
|
|
|
24
27
|
import './style.css';
|
|
25
28
|
|
|
26
29
|
const messages = defineMessages({
|
|
27
30
|
error: {
|
|
28
|
-
id:
|
|
29
|
-
'An error has occurred while editing "{name}" field. We have been notified and we are looking into it. Please save your work and retry. If the issue persists please contact the site administrator.',
|
|
31
|
+
id: 'An error has occurred while editing "{name}" field. We have been notified and we are looking into it. Please save your work and retry. If the issue persists please contact the site administrator.',
|
|
30
32
|
defaultMessage:
|
|
31
33
|
'An error has occurred while editing "{name}" field. We have been notified and we are looking into it. Please save your work and retry. If the issue persists please contact the site administrator.',
|
|
32
34
|
},
|
|
@@ -45,6 +47,8 @@ const HtmlSlateWidget = (props) => {
|
|
|
45
47
|
intl,
|
|
46
48
|
} = props;
|
|
47
49
|
|
|
50
|
+
const { slateWidgetExtensions } = config.settings.slate;
|
|
51
|
+
|
|
48
52
|
const [selected, setSelected] = React.useState(focus);
|
|
49
53
|
|
|
50
54
|
const editor = React.useMemo(() => makeEditor(), []);
|
|
@@ -128,6 +132,9 @@ const HtmlSlateWidget = (props) => {
|
|
|
128
132
|
block={block}
|
|
129
133
|
selected={selected}
|
|
130
134
|
properties={properties}
|
|
135
|
+
extensions={slateWidgetExtensions}
|
|
136
|
+
onKeyDown={handleKeyDetached}
|
|
137
|
+
editableProps={{ 'aria-multiline': 'true' }}
|
|
131
138
|
placeholder={placeholder}
|
|
132
139
|
/>
|
|
133
140
|
</ErrorBoundary>
|
|
@@ -7,7 +7,9 @@ import React from 'react';
|
|
|
7
7
|
import isUndefined from 'lodash/isUndefined';
|
|
8
8
|
import isString from 'lodash/isString';
|
|
9
9
|
import { FormFieldWrapper } from '@plone/volto/components';
|
|
10
|
+
import { handleKeyDetached } from '@plone/volto-slate/blocks/Text/keyboard';
|
|
10
11
|
import SlateEditor from '@plone/volto-slate/editor/SlateEditor';
|
|
12
|
+
import config from '@plone/volto/registry';
|
|
11
13
|
|
|
12
14
|
import { createEmptyParagraph, createParagraph } from '../utils/blocks';
|
|
13
15
|
|
|
@@ -37,6 +39,7 @@ const SlateRichTextWidget = (props) => {
|
|
|
37
39
|
readOnly = false,
|
|
38
40
|
} = props;
|
|
39
41
|
const [selected, setSelected] = React.useState(focus);
|
|
42
|
+
const { slateWidgetExtensions } = config.settings.slate;
|
|
40
43
|
|
|
41
44
|
return (
|
|
42
45
|
<FormFieldWrapper {...props} draggable={false} className="slate_wysiwyg">
|
|
@@ -62,7 +65,10 @@ const SlateRichTextWidget = (props) => {
|
|
|
62
65
|
block={block}
|
|
63
66
|
selected={selected}
|
|
64
67
|
properties={properties}
|
|
68
|
+
extensions={slateWidgetExtensions}
|
|
69
|
+
onKeyDown={handleKeyDetached}
|
|
65
70
|
placeholder={placeholder}
|
|
71
|
+
editableProps={{ 'aria-multiline': 'true' }}
|
|
66
72
|
/>
|
|
67
73
|
</div>
|
|
68
74
|
</FormFieldWrapper>
|
|
@@ -457,6 +457,7 @@ class Contents extends Component {
|
|
|
457
457
|
this.setState(
|
|
458
458
|
{
|
|
459
459
|
currentPage: 0,
|
|
460
|
+
selected: [],
|
|
460
461
|
},
|
|
461
462
|
() =>
|
|
462
463
|
this.setState({ filter: '' }, () =>
|
|
@@ -650,6 +651,7 @@ class Contents extends Component {
|
|
|
650
651
|
this.setState(
|
|
651
652
|
{
|
|
652
653
|
currentPage: value,
|
|
654
|
+
selected: [],
|
|
653
655
|
},
|
|
654
656
|
() => this.fetchContents(),
|
|
655
657
|
);
|
|
@@ -667,6 +669,7 @@ class Contents extends Component {
|
|
|
667
669
|
{
|
|
668
670
|
pageSize: value,
|
|
669
671
|
currentPage: 0,
|
|
672
|
+
selected: [],
|
|
670
673
|
},
|
|
671
674
|
() => this.fetchContents(),
|
|
672
675
|
);
|
|
@@ -726,6 +729,7 @@ class Contents extends Component {
|
|
|
726
729
|
this.setState({
|
|
727
730
|
sort_on: values[0],
|
|
728
731
|
sort_order: values[1],
|
|
732
|
+
selected: [],
|
|
729
733
|
});
|
|
730
734
|
this.props.sortContent(
|
|
731
735
|
getBaseUrl(this.props.pathname),
|
|
@@ -753,6 +757,7 @@ class Contents extends Component {
|
|
|
753
757
|
this.setState(
|
|
754
758
|
{
|
|
755
759
|
currentPage: 0,
|
|
760
|
+
selected: [],
|
|
756
761
|
},
|
|
757
762
|
() => this.fetchContents(),
|
|
758
763
|
);
|
|
@@ -778,6 +783,7 @@ class Contents extends Component {
|
|
|
778
783
|
this.setState(
|
|
779
784
|
{
|
|
780
785
|
currentPage: 0,
|
|
786
|
+
selected: [],
|
|
781
787
|
},
|
|
782
788
|
() => this.fetchContents(),
|
|
783
789
|
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
const AlternateHrefLangs = (props) => {
|
|
5
|
+
const { content } = props;
|
|
6
|
+
return (
|
|
7
|
+
<Helmet>
|
|
8
|
+
{config.settings.isMultilingual &&
|
|
9
|
+
content['@components']?.translations?.items?.map((item, key) => {
|
|
10
|
+
return (
|
|
11
|
+
<link
|
|
12
|
+
key={key}
|
|
13
|
+
rel="alternate"
|
|
14
|
+
hrefLang={item.language}
|
|
15
|
+
href={item['@id']}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
})}
|
|
19
|
+
</Helmet>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { AlternateHrefLangs };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
import renderer from 'react-test-renderer';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import config from '@plone/volto/registry';
|
|
8
|
+
|
|
9
|
+
import { AlternateHrefLangs } from './AlternateHrefLangs';
|
|
10
|
+
|
|
11
|
+
const mockStore = configureStore();
|
|
12
|
+
|
|
13
|
+
describe('AlternateHrefLangs', () => {
|
|
14
|
+
beforeEach(() => {});
|
|
15
|
+
it('non multilingual site, renders nothing', () => {
|
|
16
|
+
config.settings.isMultilingual = false;
|
|
17
|
+
const content = {
|
|
18
|
+
'@id': '/',
|
|
19
|
+
'@components': {},
|
|
20
|
+
};
|
|
21
|
+
const store = mockStore({
|
|
22
|
+
intl: {
|
|
23
|
+
locale: 'en',
|
|
24
|
+
messages: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// We need to force the component rendering
|
|
28
|
+
// to fill the Helmet
|
|
29
|
+
renderer.create(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<AlternateHrefLangs content={content} />
|
|
32
|
+
</Provider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
36
|
+
expect(helmetLinks.length).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
it('multilingual site, with some translations', () => {
|
|
39
|
+
config.settings.isMultilingual = true;
|
|
40
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
41
|
+
|
|
42
|
+
const content = {
|
|
43
|
+
'@components': {
|
|
44
|
+
translations: {
|
|
45
|
+
items: [
|
|
46
|
+
{ '@id': '/en', language: 'en' },
|
|
47
|
+
{ '@id': '/es', language: 'es' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const store = mockStore({
|
|
54
|
+
intl: {
|
|
55
|
+
locale: 'en',
|
|
56
|
+
messages: {},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// We need to force the component rendering
|
|
61
|
+
// to fill the Helmet
|
|
62
|
+
renderer.create(
|
|
63
|
+
<Provider store={store}>
|
|
64
|
+
<>
|
|
65
|
+
<AlternateHrefLangs content={content} />
|
|
66
|
+
</>
|
|
67
|
+
</Provider>,
|
|
68
|
+
);
|
|
69
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
70
|
+
|
|
71
|
+
expect(helmetLinks.length).toBe(2);
|
|
72
|
+
|
|
73
|
+
expect(helmetLinks).toContainEqual({
|
|
74
|
+
rel: 'alternate',
|
|
75
|
+
href: '/es',
|
|
76
|
+
hrefLang: 'es',
|
|
77
|
+
});
|
|
78
|
+
expect(helmetLinks).toContainEqual({
|
|
79
|
+
rel: 'alternate',
|
|
80
|
+
href: '/en',
|
|
81
|
+
hrefLang: 'en',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('multilingual site, with all available translations', () => {
|
|
85
|
+
config.settings.isMultilingual = true;
|
|
86
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
87
|
+
const store = mockStore({
|
|
88
|
+
intl: {
|
|
89
|
+
locale: 'en',
|
|
90
|
+
messages: {},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const content = {
|
|
95
|
+
'@components': {
|
|
96
|
+
translations: {
|
|
97
|
+
items: [
|
|
98
|
+
{ '@id': '/en', language: 'en' },
|
|
99
|
+
{ '@id': '/eu', language: 'eu' },
|
|
100
|
+
{ '@id': '/es', language: 'es' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// We need to force the component rendering
|
|
107
|
+
// to fill the Helmet
|
|
108
|
+
renderer.create(
|
|
109
|
+
<Provider store={store}>
|
|
110
|
+
<AlternateHrefLangs content={content} />
|
|
111
|
+
</Provider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
115
|
+
|
|
116
|
+
// We expect having 3 links
|
|
117
|
+
expect(helmetLinks.length).toBe(3);
|
|
118
|
+
|
|
119
|
+
expect(helmetLinks).toContainEqual({
|
|
120
|
+
rel: 'alternate',
|
|
121
|
+
href: '/eu',
|
|
122
|
+
hrefLang: 'eu',
|
|
123
|
+
});
|
|
124
|
+
expect(helmetLinks).toContainEqual({
|
|
125
|
+
rel: 'alternate',
|
|
126
|
+
href: '/es',
|
|
127
|
+
hrefLang: 'es',
|
|
128
|
+
});
|
|
129
|
+
expect(helmetLinks).toContainEqual({
|
|
130
|
+
rel: 'alternate',
|
|
131
|
+
href: '/en',
|
|
132
|
+
hrefLang: 'en',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -54,7 +54,14 @@ export default function Image({
|
|
|
54
54
|
attrs.className = cx(className, { responsive });
|
|
55
55
|
|
|
56
56
|
if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
|
|
57
|
-
const sortedScales = Object.values(
|
|
57
|
+
const sortedScales = Object.values({
|
|
58
|
+
...image.scales,
|
|
59
|
+
original: {
|
|
60
|
+
download: `${image.download}`,
|
|
61
|
+
width: image.width,
|
|
62
|
+
height: image.height,
|
|
63
|
+
},
|
|
64
|
+
}).sort((a, b) => {
|
|
58
65
|
if (a.width > b.width) return 1;
|
|
59
66
|
else if (a.width < b.width) return -1;
|
|
60
67
|
else return 0;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Tags,
|
|
19
19
|
Toolbar,
|
|
20
20
|
} from '@plone/volto/components';
|
|
21
|
+
import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';
|
|
21
22
|
import { listActions, getContent } from '@plone/volto/actions';
|
|
22
23
|
import {
|
|
23
24
|
BodyClass,
|
|
@@ -238,6 +239,7 @@ class View extends Component {
|
|
|
238
239
|
return (
|
|
239
240
|
<div id="view">
|
|
240
241
|
<ContentMetadataTags content={this.props.content} />
|
|
242
|
+
<AlternateHrefLangs content={this.props.content} />
|
|
241
243
|
{/* Body class if displayName in component is set */}
|
|
242
244
|
<BodyClass
|
|
243
245
|
className={
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function AlternateHrefLangs(props: any): JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|